Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix(caldav): Add default reminder to attendees of scheduling messages
Signed-off-by: Lucas Ferreira da Silva <[email protected]>
  • Loading branch information
lufer22 committed Sep 19, 2024
commit 3b9e6a9e7df1fdabaff36b804dd42e36e3db2af7
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) {

Check notice

Code scanning / Psalm

DocblockTypeContradiction

Operand of type false is always falsy

Check notice

Code scanning / Psalm

DocblockTypeContradiction

Docblock-defined type Sabre\DAV\ServerPlugin for $aclPlugin is never falsy
return;
}

$caldavNS = '{'.self::NS_CALDAV.'}';

$principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);

Check failure

Code scanning / Psalm

UndefinedMethod

Method Sabre\DAV\ServerPlugin::getPrincipalByUri does not exist
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']);

Check notice

Code scanning / Psalm

InvalidArgument

Argument 2 of Sabre\DAV\Server::removeListener expects callable, but list{Sabre\DAV\ServerPlugin, 'propFind'} provided

$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);

Check notice

Code scanning / Psalm

InvalidArgument

Argument 2 of Sabre\DAV\Server::on expects callable, but list{Sabre\DAV\ServerPlugin, 'propFind'} provided

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)) {

Check failure

Code scanning / Psalm

UndefinedMethod

Method Sabre\DAV\ServerPlugin::checkPrivileges does not exist
$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);

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod

Method Sabre\DAV\INode::getCalendarObjectByUID does not exist
if ($result) {
// There was an existing object, we need to update probably.
$objectPath = $homePath.'/'.$result;
$objectNode = $this->server->tree->getNodeForPath($objectPath);
$oldICalendarData = $objectNode->get();

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod

Method Sabre\DAV\INode::get does not exist
$currentObject = Reader::read($oldICalendarData);
} else {
$isNewNode = true;
}

$broker = new ITip\Broker();
$newObject = $broker->processMessage($iTipMessage, $currentObject);

Check notice

Code scanning / Psalm

ArgumentTypeCoercion

Argument 2 of Sabre\VObject\ITip\Broker::processMessage expects Sabre\VObject\Component\VCalendar|null, but parent type Sabre\VObject\Document|null provided

$inbox->createFile($newFileName, $iTipMessage->message->serialize());

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod

Method Sabre\DAV\INode::createFile does not exist

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());

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod

Method Sabre\DAV\INode::createFile does not exist
} 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());

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod

Method Sabre\DAV\INode::put does not exist

Check notice

Code scanning / Psalm

PossiblyNullReference

Cannot call method put on possibly null value
}
$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) {

Check notice

Code scanning / Psalm

MissingReturnType

Method OCA\DAV\CalDAV\Schedule\Plugin::createAlarm does not have a return type, expecting void
$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);

Check notice

Code scanning / Psalm

PossiblyNullReference

Cannot call method add on possibly null value

Check failure

Code scanning / Psalm

InvalidArgument

Argument 1 of Sabre\VObject\Property::add expects string, but Sabre\VObject\Component provided
}

/**
* 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';
}
}