Skip to content

Commit

Permalink
fix(caldav): Add default reminder to attendees of scheduling messages
Browse files Browse the repository at this point in the history
Signed-off-by: Lucas Ferreira da Silva <[email protected]>
  • Loading branch information
lufer22 committed Sep 19, 2024
1 parent 00a27af commit 3b9e6a9
Showing 1 changed file with 190 additions and 1 deletion.
191 changes: 190 additions & 1 deletion apps/dav/lib/CalDAV/Schedule/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,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;
Expand Down Expand Up @@ -229,7 +230,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
Expand Down Expand Up @@ -444,6 +445,159 @@ 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.
*/
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;

$userDefaultReminder = $this->config->getUserValue($this->stripOffMailTo($iTipMessage->recipient), '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.
*
Expand Down Expand Up @@ -734,4 +888,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';
}
}

0 comments on commit 3b9e6a9

Please sign in to comment.