diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php index 48f4cbf22ef0a..916c854731b80 100644 --- a/apps/dav/lib/CalDAV/Schedule/Plugin.php +++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php @@ -11,6 +11,7 @@ use OCA\DAV\CalDAV\CalendarHome; use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCP\IConfig; +use OCP\IUserManager; use Psr\Log\LoggerInterface; use Sabre\CalDAV\ICalendar; use Sabre\CalDAV\ICalendarObject; @@ -21,6 +22,7 @@ use Sabre\DAV\PropFind; use Sabre\DAV\Server; use Sabre\DAV\Xml\Property\LocalHref; +use Sabre\DAVACL; use Sabre\DAVACL\IACL; use Sabre\DAVACL\IPrincipal; use Sabre\HTTP\RequestInterface; @@ -44,6 +46,11 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { */ private $config; + /** + * @var IUserManager + */ + private $userManager; + /** @var ITip\Message[] */ private $schedulingResponses = []; @@ -58,10 +65,11 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { /** * @param IConfig $config */ - public function __construct(IConfig $config, LoggerInterface $logger, DefaultCalendarValidator $defaultCalendarValidator) { + public function __construct(IConfig $config, LoggerInterface $logger, DefaultCalendarValidator $defaultCalendarValidator, IUserManager $userManager) { $this->config = $config; $this->logger = $logger; $this->defaultCalendarValidator = $defaultCalendarValidator; + $this->userManager = $userManager; } /** @@ -229,7 +237,7 @@ public function scheduleLocalDelivery(ITip\Message $iTipMessage):void { $vevent->remove('VALARM'); } - parent::scheduleLocalDelivery($iTipMessage); + $this->scheduleLocalDeliveryHandler($iTipMessage); // We only care when the message was successfully delivered locally // Log all possible codes returned from the parent method that mean something went wrong // 3.7, 3.8, 5.0, 5.2 @@ -444,6 +452,166 @@ public function propFindDefaultCalendarUrl(PropFind $propFind, INode $node) { } } + /** + * Event handler for the 'schedule' event. + * + * This handler attempts to look at local accounts to deliver the + * scheduling object. + * + * @param ITip\Message $iTipMessage + * @return void + */ + public function scheduleLocalDeliveryHandler(ITip\Message $iTipMessage) + { + $aclPlugin = $this->server->getPlugin('acl'); + + // Local delivery is not available if the ACL plugin is not loaded. + if (!$aclPlugin) { + return; + } + + $caldavNS = '{'.self::NS_CALDAV.'}'; + + $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient); + if (!$principalUri) { + $iTipMessage->scheduleStatus = '3.7;Could not find principal.'; + + return; + } + + // We found a principal URL, now we need to find its inbox. + // Unfortunately we may not have sufficient privileges to find this, so + // we are temporarily turning off ACL to let this come through. + // + // Once we support PHP 5.5, this should be wrapped in a try..finally + // block so we can ensure that this privilege gets added again after. + $this->server->removeListener('propFind', [$aclPlugin, 'propFind']); + + $result = $this->server->getProperties( + $principalUri, + [ + '{DAV:}principal-URL', + $caldavNS.'calendar-home-set', + $caldavNS.'schedule-inbox-URL', + $caldavNS.'schedule-default-calendar-URL', + '{http://sabredav.org/ns}email-address', + ] + ); + + // Re-registering the ACL event + $this->server->on('propFind', [$aclPlugin, 'propFind'], 20); + + if (!isset($result[$caldavNS.'schedule-inbox-URL'])) { + $iTipMessage->scheduleStatus = '5.2;Could not find local inbox'; + + return; + } + if (!isset($result[$caldavNS.'calendar-home-set'])) { + $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set'; + + return; + } + if (!isset($result[$caldavNS.'schedule-default-calendar-URL'])) { + $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property'; + + return; + } + + $calendarPath = $result[$caldavNS.'schedule-default-calendar-URL']->getHref(); + $homePath = $result[$caldavNS.'calendar-home-set']->getHref(); + $inboxPath = $result[$caldavNS.'schedule-inbox-URL']->getHref(); + + if ('REPLY' === $iTipMessage->method) { + $privilege = 'schedule-deliver-reply'; + } else { + $privilege = 'schedule-deliver-invite'; + } + + if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS.$privilege, DAVACL\Plugin::R_PARENT, false)) { + $iTipMessage->scheduleStatus = '3.8;insufficient privileges: '.$privilege.' is required on the recipient schedule inbox.'; + + return; + } + + // Next, we're going to find out if the item already exits in one of + // the users' calendars. + $uid = $iTipMessage->uid; + + $newFileName = 'sabredav-'.\Sabre\DAV\UUIDUtil::getUUID().'.ics'; + + $home = $this->server->tree->getNodeForPath($homePath); + $inbox = $this->server->tree->getNodeForPath($inboxPath); + + $currentObject = null; + $objectNode = null; + $oldICalendarData = null; + $isNewNode = false; + + $result = $this->userManager->getByEmail($this->stripOffMailTo($iTipMessage->recipient)); + $user = isset($result[0]) ? $result[0] : null; + if ($user) { + $userDefaultReminder = $this->config->getUserValue($user->getUID(), 'calendar', 'defaultReminder', 'none'); + // If the user hasn't changed the default reminder, it will use the global one + if ($userDefaultReminder === 'none') { + $userDefaultReminder = $this->config->getAppValue('calendar', 'defaultReminder', 'none'); + } + if ($userDefaultReminder !== 'none') { + $userDefaultReminder = intval($userDefaultReminder); + $this->createAlarm($iTipMessage, $userDefaultReminder); + } + } + + $result = $home->getCalendarObjectByUID($uid); + if ($result) { + // There was an existing object, we need to update probably. + $objectPath = $homePath.'/'.$result; + $objectNode = $this->server->tree->getNodeForPath($objectPath); + $oldICalendarData = $objectNode->get(); + $currentObject = Reader::read($oldICalendarData); + } else { + $isNewNode = true; + } + + $broker = new ITip\Broker(); + $newObject = $broker->processMessage($iTipMessage, $currentObject); + + $inbox->createFile($newFileName, $iTipMessage->message->serialize()); + + if (!$newObject) { + // We received an iTip message referring to a UID that we don't + // have in any calendars yet, and processMessage did not give us a + // calendarobject back. + // + // The implication is that processMessage did not understand the + // iTip message. + $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.'; + + return; + } + + // Note that we are bypassing ACL on purpose by calling this directly. + // We may need to look a bit deeper into this later. Supporting ACL + // here would be nice. + if ($isNewNode) { + $calendar = $this->server->tree->getNodeForPath($calendarPath); + $calendar->createFile($newFileName, $newObject->serialize()); + } else { + // If the message was a reply, we may have to inform other + // attendees of this attendees status. Therefore we're shooting off + // another itipMessage. + if ('REPLY' === $iTipMessage->method) { + $this->processICalendarChange( + $oldICalendarData, + $newObject, + [$iTipMessage->recipient], + [$iTipMessage->sender] + ); + } + $objectNode->put($newObject->serialize()); + } + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + } + /** * Returns a list of addresses that are associated with a principal. * @@ -734,4 +902,39 @@ private function handleSameOrganizerException( } } } + + /** + * Creates a VALARM inside an iTipMessage + * + * @param ITip\Message $iTipMessage + * @param int $userDefaultReminder + */ + private function createAlarm(ITip\Message $iTipMessage, int $userDefaultReminder) { + $alarm = $iTipMessage->message->createComponent('VALARM'); + $alarm->add($iTipMessage->message->createProperty('TRIGGER', '-' . $this->secondsToIso8601Duration(abs($userDefaultReminder)), ['RELATED' => 'START'])); + $alarm->add($iTipMessage->message->createProperty('ACTION', 'DISPLAY')); + $iTipMessage->message->VEVENT->add($alarm); + } + + /** + * Converts seconds to an ISO 8601 duration string + * + * @param int $secs + * @return string + */ + private function secondsToIso8601Duration(int $secs): string { + $day = 24 * 60 * 60; + $hour = 60 * 60; + $minute = 60; + if ($secs % $day === 0) { + return 'P' . $secs / $day . 'D'; + } + if ($secs % $hour === 0) { + return 'PT' . $secs / $hour . 'H'; + } + if ($secs % $minute === 0) { + return 'PT' . $secs / $minute . 'M'; + } + return 'PT' . $secs . 'S'; + } } diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 2784e99b5f73b..f2cd1e5785509 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -60,6 +60,7 @@ use OCP\IConfig; use OCP\IPreview; use OCP\IRequest; +use OCP\IUserManager; use OCP\IUserSession; use OCP\Profiler\IProfiler; use OCP\SabrePluginEvent; @@ -162,7 +163,7 @@ public function __construct(IRequest $request, string $baseUri) { $this->server->addPlugin(new DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest(), \OC::$server->getConfig())); $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin()); $this->server->addPlugin(new \OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin(\OC::$server->getConfig(), $logger)); - $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class), \OC::$server->get(DefaultCalendarValidator::class))); + $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class), \OC::$server->get(DefaultCalendarValidator::class), \OC::$server->get(IUserManager::class))); $this->server->addPlugin(\OC::$server->get(\OCA\DAV\CalDAV\Trashbin\Plugin::class)); $this->server->addPlugin(new \OCA\DAV\CalDAV\WebcalCaching\Plugin($request));