Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(caldav): Add default reminder to attendees of scheduling messages #48226

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
207 changes: 205 additions & 2 deletions apps/dav/lib/CalDAV/Schedule/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -44,6 +46,11 @@
*/
private $config;

/**
* @var IUserManager
*/
private $userManager;

/** @var ITip\Message[] */
private $schedulingResponses = [];

Expand All @@ -58,10 +65,11 @@
/**
* @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;
}

/**
Expand Down Expand Up @@ -229,7 +237,7 @@
$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 +452,166 @@
}
}

/**
* 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)
Fixed Show fixed Hide fixed
{
$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 Note

Operand of type false is always falsy

Check notice

Code scanning / Psalm

DocblockTypeContradiction Note

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

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

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

Check failure on line 475 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMethod

apps/dav/lib/CalDAV/Schedule/Plugin.php:475:31: UndefinedMethod: Method Sabre\DAV\ServerPlugin::getPrincipalByUri does not exist (see https://psalm.dev/022)

Check failure

Code scanning / Psalm

UndefinedMethod Error

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 Note

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 Note

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 on line 530 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMethod

apps/dav/lib/CalDAV/Schedule/Plugin.php:530:20: UndefinedMethod: Method Sabre\DAV\ServerPlugin::checkPrivileges does not exist (see https://psalm.dev/022)

Check failure

Code scanning / Psalm

UndefinedMethod Error

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;

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

Check notice

Code scanning / Psalm

DeprecatedMethod Note

The method OCP\IConfig::getAppValue has been marked as deprecated
}
if ($userDefaultReminder !== 'none') {
$userDefaultReminder = intval($userDefaultReminder);
$this->createAlarm($iTipMessage, $userDefaultReminder);
}
}

$result = $home->getCalendarObjectByUID($uid);

Check failure on line 564 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedInterfaceMethod

apps/dav/lib/CalDAV/Schedule/Plugin.php:564:20: UndefinedInterfaceMethod: Method Sabre\DAV\INode::getCalendarObjectByUID does not exist (see https://psalm.dev/181)

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod Error

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 on line 569 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedInterfaceMethod

apps/dav/lib/CalDAV/Schedule/Plugin.php:569:37: UndefinedInterfaceMethod: Method Sabre\DAV\INode::get does not exist (see https://psalm.dev/181)

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod Error

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 Note

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 on line 578 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedInterfaceMethod

apps/dav/lib/CalDAV/Schedule/Plugin.php:578:11: UndefinedInterfaceMethod: Method Sabre\DAV\INode::createFile does not exist (see https://psalm.dev/181)

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod Error

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 on line 597 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedInterfaceMethod

apps/dav/lib/CalDAV/Schedule/Plugin.php:597:15: UndefinedInterfaceMethod: Method Sabre\DAV\INode::createFile does not exist (see https://psalm.dev/181)

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod Error

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 on line 610 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedInterfaceMethod

apps/dav/lib/CalDAV/Schedule/Plugin.php:610:17: UndefinedInterfaceMethod: Method Sabre\DAV\INode::put does not exist (see https://psalm.dev/181)

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod Error

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

Check notice

Code scanning / Psalm

PossiblyNullReference Note

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 @@ -541,7 +709,7 @@
// and Sabre\CalDAV\Schedule\Plugin::getFreeBusyForEmail

$aclPlugin = $this->server->getPlugin('acl');
$this->server->removeListener('propFind', [$aclPlugin, 'propFind']);

Check failure on line 712 in apps/dav/lib/CalDAV/Schedule/Plugin.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidArgument

apps/dav/lib/CalDAV/Schedule/Plugin.php:712:45: InvalidArgument: Argument 2 of Sabre\DAV\Server::removeListener expects callable, but list{Sabre\DAV\ServerPlugin, 'propFind'} provided (see https://psalm.dev/004)

$result = $aclPlugin->principalSearch(
['{http://sabredav.org/ns}email-address' => $this->stripOffMailTo($email)],
Expand Down Expand Up @@ -734,4 +902,39 @@
}
}
}

/**
* 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 Note

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 Note

Cannot call method add on possibly null value

Check failure

Code scanning / Psalm

InvalidArgument Error

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';
}
}
3 changes: 2 additions & 1 deletion apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
Loading