Include accept / decline links in CalDAV invitation emails

Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
Georg Ehrke 2018-06-19 21:01:14 +02:00 committed by Roeland Jago Douma
parent 3ff3141a1e
commit 4aa4e4080c
No known key found for this signature in database
GPG key ID: F941078878347C0C
17 changed files with 1344 additions and 9 deletions

View file

@ -22,6 +22,7 @@
<background-jobs>
<job>OCA\DAV\BackgroundJob\CleanupDirectLinksJob</job>
<job>OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob</job>
<job>OCA\DAV\BackgroundJob\CleanupInvitationTokenJob</job>
</background-jobs>
<repair-steps>

View file

@ -25,6 +25,10 @@ return [
'routes' => [
['name' => 'birthday_calendar#enable', 'url' => '/enableBirthdayCalendar', 'verb' => 'POST'],
['name' => 'birthday_calendar#disable', 'url' => '/disableBirthdayCalendar', 'verb' => 'POST'],
['name' => 'invitation_response#accept', 'url' => '/invitation/accept/{token}', 'verb' => 'GET'],
['name' => 'invitation_response#decline', 'url' => '/invitation/decline/{token}', 'verb' => 'GET'],
['name' => 'invitation_response#options', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'GET'],
['name' => 'invitation_response#processMoreOptionsResult', 'url' => '/invitation/moreOptions/{token}', 'verb' => 'POST']
],
'ocs' => [
['name' => 'direct#getUrl', 'url' => '/api/v1/direct', 'verb' => 'POST'],

View file

@ -12,6 +12,7 @@ return array(
'OCA\\DAV\\Avatars\\AvatarNode' => $baseDir . '/../lib/Avatars/AvatarNode.php',
'OCA\\DAV\\Avatars\\RootCollection' => $baseDir . '/../lib/Avatars/RootCollection.php',
'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => $baseDir . '/../lib/BackgroundJob/CleanupDirectLinksJob.php',
'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php',
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
'OCA\\DAV\\BackgroundJob\\UpdateCalendarResourcesRoomsBackgroundJob' => $baseDir . '/../lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php',
'OCA\\DAV\\CalDAV\\Activity\\Backend' => $baseDir . '/../lib/CalDAV/Activity/Backend.php',
@ -33,6 +34,7 @@ return array(
'OCA\\DAV\\CalDAV\\CalendarManager' => $baseDir . '/../lib/CalDAV/CalendarManager.php',
'OCA\\DAV\\CalDAV\\CalendarObject' => $baseDir . '/../lib/CalDAV/CalendarObject.php',
'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php',
'OCA\\DAV\\CalDAV\\InvitationResponse\\InvitationResponseServer' => $baseDir . '/../lib/CalDAV/InvitationResponse/InvitationResponseServer.php',
'OCA\\DAV\\CalDAV\\Plugin' => $baseDir . '/../lib/CalDAV/Plugin.php',
'OCA\\DAV\\CalDAV\\Principal\\Collection' => $baseDir . '/../lib/CalDAV/Principal/Collection.php',
'OCA\\DAV\\CalDAV\\Principal\\User' => $baseDir . '/../lib/CalDAV/Principal/User.php',
@ -117,6 +119,7 @@ return array(
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => $baseDir . '/../lib/Connector/Sabre/TagsPlugin.php',
'OCA\\DAV\\Controller\\BirthdayCalendarController' => $baseDir . '/../lib/Controller/BirthdayCalendarController.php',
'OCA\\DAV\\Controller\\DirectController' => $baseDir . '/../lib/Controller/DirectController.php',
'OCA\\DAV\\Controller\\InvitationResponseController' => $baseDir . '/../lib/Controller/InvitationResponseController.php',
'OCA\\DAV\\DAV\\CustomPropertiesBackend' => $baseDir . '/../lib/DAV/CustomPropertiesBackend.php',
'OCA\\DAV\\DAV\\GroupPrincipalBackend' => $baseDir . '/../lib/DAV/GroupPrincipalBackend.php',
'OCA\\DAV\\DAV\\PublicAuth' => $baseDir . '/../lib/DAV/PublicAuth.php',
@ -150,6 +153,7 @@ return array(
'OCA\\DAV\\Migration\\Version1004Date20170926103422' => $baseDir . '/../lib/Migration/Version1004Date20170926103422.php',
'OCA\\DAV\\Migration\\Version1005Date20180413093149' => $baseDir . '/../lib/Migration/Version1005Date20180413093149.php',
'OCA\\DAV\\Migration\\Version1005Date20180530124431' => $baseDir . '/../lib/Migration/Version1005Date20180530124431.php',
'OCA\\DAV\\Migration\\Version1006Date20180619154313' => $baseDir . '/../lib/Migration/Version1006Date20180619154313.php',
'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php',
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',

View file

@ -27,6 +27,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Avatars\\AvatarNode' => __DIR__ . '/..' . '/../lib/Avatars/AvatarNode.php',
'OCA\\DAV\\Avatars\\RootCollection' => __DIR__ . '/..' . '/../lib/Avatars/RootCollection.php',
'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectLinksJob.php',
'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php',
'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php',
'OCA\\DAV\\BackgroundJob\\UpdateCalendarResourcesRoomsBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php',
'OCA\\DAV\\CalDAV\\Activity\\Backend' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Backend.php',
@ -48,6 +49,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\CalendarManager' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarManager.php',
'OCA\\DAV\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarObject.php',
'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php',
'OCA\\DAV\\CalDAV\\InvitationResponse\\InvitationResponseServer' => __DIR__ . '/..' . '/../lib/CalDAV/InvitationResponse/InvitationResponseServer.php',
'OCA\\DAV\\CalDAV\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Plugin.php',
'OCA\\DAV\\CalDAV\\Principal\\Collection' => __DIR__ . '/..' . '/../lib/CalDAV/Principal/Collection.php',
'OCA\\DAV\\CalDAV\\Principal\\User' => __DIR__ . '/..' . '/../lib/CalDAV/Principal/User.php',
@ -132,6 +134,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/TagsPlugin.php',
'OCA\\DAV\\Controller\\BirthdayCalendarController' => __DIR__ . '/..' . '/../lib/Controller/BirthdayCalendarController.php',
'OCA\\DAV\\Controller\\DirectController' => __DIR__ . '/..' . '/../lib/Controller/DirectController.php',
'OCA\\DAV\\Controller\\InvitationResponseController' => __DIR__ . '/..' . '/../lib/Controller/InvitationResponseController.php',
'OCA\\DAV\\DAV\\CustomPropertiesBackend' => __DIR__ . '/..' . '/../lib/DAV/CustomPropertiesBackend.php',
'OCA\\DAV\\DAV\\GroupPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/GroupPrincipalBackend.php',
'OCA\\DAV\\DAV\\PublicAuth' => __DIR__ . '/..' . '/../lib/DAV/PublicAuth.php',
@ -165,6 +168,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Migration\\Version1004Date20170926103422' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170926103422.php',
'OCA\\DAV\\Migration\\Version1005Date20180413093149' => __DIR__ . '/..' . '/../lib/Migration/Version1005Date20180413093149.php',
'OCA\\DAV\\Migration\\Version1005Date20180530124431' => __DIR__ . '/..' . '/../lib/Migration/Version1005Date20180530124431.php',
'OCA\\DAV\\Migration\\Version1006Date20180619154313' => __DIR__ . '/..' . '/../lib/Migration/Version1006Date20180619154313.php',
'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php',
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',

View file

@ -0,0 +1,78 @@
/* Database selector on install page */
form #selectPartStatForm {
text-align:center;
white-space: nowrap;
margin: 0;
}
form #selectPartStatForm .info {
white-space: normal;
}
form #selectPartStatForm input[type="radio"] {
display: none;
}
form #selectPartStatForm input[type="radio"]:checked+label {
background-color: #e8e8e8;
}
form #selectPartStatForm input[type="radio"]:checked ~ form fieldset#more_options {
display: none;
}
form #selectPartStatForm label {
color: #000;
background-color: #f8f8f8;
position: static;
margin: 0 -3px 5px;
cursor:pointer;
border: 1px solid #ddd;
display: inline-block;
padding: 0;
line-height: normal;
vertical-align: middle;
text-align: center;
overflow: visible;
}
form #selectPartStatForm label:first-of-type {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
form #selectPartStatForm label:last-of-type {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
form #selectPartStatForm label span {
cursor: pointer;
padding: 10px 20px;
display: block;
line-height: normal;
}
form #selectPartStatForm label.ui-state-hover,
form #selectPartStatForm label.ui-state-active {
color:#000;
background-color:#e8e8e8;
}
form input[type="number"] {
width: 249px;
background: #fff;
color: #555;
cursor: text;
font-family: inherit;
-webkit-appearance: textfield;
-moz-appearance: textfield;
box-sizing: content-box;
border: none;
font-weight: 300;
}
form input[type="submit"] {
display: block;
margin: 0 auto;
padding: 11px 20px 9px
}

View file

@ -0,0 +1,3 @@
// $(document).ready(function() {
// $('#selectPartStatForm').buttonset();
// });

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OC\BackgroundJob\TimedJob;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IDBConnection;
class CleanupInvitationTokenJob extends TimedJob {
/** @var IDBConnection */
private $db;
/** @var ITimeFactory */
private $timeFactory;
public function __construct(IDBConnection $db, ITimeFactory $timeFactory) {
$this->db = $db;
$this->timeFactory = $timeFactory;
$this->setInterval(60 * 60 * 24);
}
public function run($argument) {
$query = $this->db->getQueryBuilder();
$query->delete('calendar_invitation_tokens')
->where($query->expr()->lt('expiration',
$query->createNamedParameter($this->timeFactory->getTime())))
->execute();
}
}

View file

@ -0,0 +1,118 @@
<?php
/**
* @copyright Copyright (c) 2018, Georg Ehrke.
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV\InvitationResponse;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\DavAclPlugin;
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
use OCA\DAV\RootCollection;
use OCP\SabrePluginEvent;
use Sabre\DAV\Auth\Plugin;
use OCA\DAV\AppInfo\PluginManager;
use Sabre\VObject\ITip\Message;
class InvitationResponseServer {
/** @var \OCA\DAV\Connector\Sabre\Server */
public $server;
/**
* InvitationResponseServer constructor.
*/
public function __construct() {
$baseUri = \OC::$WEBROOT . '/remote.php/dav/';
$logger = \OC::$server->getLogger();
$dispatcher = \OC::$server->getEventDispatcher();
$root = new RootCollection();
$this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
// Add maintenance plugin
$this->server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig()));
// Set URL explicitly due to reverse-proxy situations
$this->server->httpRequest->setUrl($baseUri);
$this->server->setBaseUri($baseUri);
$this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
$this->server->addPlugin(new AnonymousOptionsPlugin());
$this->server->addPlugin(new class() extends Plugin {
public function getCurrentPrincipal() {
return 'principals/system/public';
}
});
// allow setup of additional auth backends
$event = new SabrePluginEvent($this->server);
$dispatcher->dispatch('OCA\DAV\Connector\Sabre::authInit', $event);
$this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger));
$this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
$this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
// acl
$acl = new DavAclPlugin();
$acl->principalCollectionSet = [
'principals/users', 'principals/groups'
];
$acl->defaultUsernamePath = 'principals/users';
$this->server->addPlugin($acl);
// calendar plugins
$this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
$this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
//$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
$this->server->addPlugin(new \OCA\DAV\CalDAV\Publishing\PublishPlugin(
\OC::$server->getConfig(),
\OC::$server->getURLGenerator()
));
// wait with registering these until auth is handled and the filesystem is setup
$this->server->on('beforeMethod', function () use ($root) {
// register plugins from apps
$pluginManager = new PluginManager(
\OC::$server,
\OC::$server->getAppManager()
);
foreach ($pluginManager->getAppPlugins() as $appPlugin) {
$this->server->addPlugin($appPlugin);
}
foreach ($pluginManager->getAppCollections() as $appCollection) {
$root->addChild($appCollection);
}
});
}
/**
* @param Message $iTipMessage
* @return void
*/
public function handleITipMessage(Message $iTipMessage) {
/** @var \OCA\DAV\CalDAV\Schedule\Plugin $schedulingPlugin */
$schedulingPlugin = $this->server->getPlugin('caldav-schedule');
$schedulingPlugin->scheduleLocalDelivery($iTipMessage);
}
}

View file

@ -28,12 +28,14 @@ namespace OCA\DAV\CalDAV\Schedule;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\L10N\IFactory as L10NFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Security\ISecureRandom;
use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
@ -79,6 +81,12 @@ class IMipPlugin extends SabreIMipPlugin {
/** @var IURLGenerator */
private $urlGenerator;
/** @var ISecureRandom */
private $random;
/** @var IDBConnection */
private $db;
/** @var Defaults */
private $defaults;
@ -96,9 +104,14 @@ class IMipPlugin extends SabreIMipPlugin {
* @param L10NFactory $l10nFactory
* @param IUrlGenerator $urlGenerator
* @param Defaults $defaults
* @param ISecureRandom $random
* @param IDBConnection $db
* @param string $userId
*/
public function __construct(IConfig $config, IMailer $mailer, ILogger $logger, ITimeFactory $timeFactory, L10NFactory $l10nFactory, IURLGenerator $urlGenerator, Defaults $defaults, $userId) {
public function __construct(IConfig $config, IMailer $mailer, ILogger $logger,
ITimeFactory $timeFactory, L10NFactory $l10nFactory,
IURLGenerator $urlGenerator, Defaults $defaults,
ISecureRandom $random, IDBConnection $db, $userId) {
parent::__construct('');
$this->userId = $userId;
$this->config = $config;
@ -107,6 +120,8 @@ class IMipPlugin extends SabreIMipPlugin {
$this->timeFactory = $timeFactory;
$this->l10nFactory = $l10nFactory;
$this->urlGenerator = $urlGenerator;
$this->random = $random;
$this->db = $db;
$this->defaults = $defaults;
}
@ -138,7 +153,9 @@ class IMipPlugin extends SabreIMipPlugin {
}
// don't send out mails for events that already took place
if ($this->isEventInThePast($iTipMessage->message)) {
$lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
$currentTime = $this->timeFactory->getTime();
if ($lastOccurrence < $currentTime) {
return;
}
@ -222,6 +239,7 @@ class IMipPlugin extends SabreIMipPlugin {
$meetingAttendeeName, $meetingInviteeName);
$this->addBulletList($template, $l10n, $meetingWhen, $meetingLocation,
$meetingDescription, $meetingUrl);
$this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
$template->addFooter();
$message->useTemplate($template);
@ -249,9 +267,9 @@ class IMipPlugin extends SabreIMipPlugin {
/**
* check if event took place in the past already
* @param VCalendar $vObject
* @return bool
* @return int
*/
private function isEventInThePast(VCalendar $vObject) {
private function getLastOccurrence(VCalendar $vObject) {
/** @var VEvent $component */
$component = $vObject->VEVENT;
@ -291,8 +309,7 @@ class IMipPlugin extends SabreIMipPlugin {
}
}
$currentTime = $this->timeFactory->getTime();
return $lastOccurrence < $currentTime;
return $lastOccurrence;
}
@ -459,6 +476,38 @@ class IMipPlugin extends SabreIMipPlugin {
}
}
/**
* @param IEMailTemplate $template
* @param IL10N $l10n
* @param Message $iTipMessage
* @param int $lastOccurrence
*/
private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
Message $iTipMessage, $lastOccurrence) {
$token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
$template->addBodyButtonGroup(
$l10n->t('Accept'),
$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
'token' => $token,
]),
$l10n->t('Decline'),
$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
'token' => $token,
])
);
$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
'token' => $token,
]);
$html = vsprintf('<small><a href="%s">%s</a></small>', [
$moreOptionsURL, $l10n->t('More options ...')
]);
$text = $l10n->t('More options at %s', [$moreOptionsURL]);
$template->addBodyText($html, $text);
}
/**
* @param string $path
* @return string
@ -468,4 +517,37 @@ class IMipPlugin extends SabreIMipPlugin {
$this->urlGenerator->imagePath('core', $path)
);
}
/**
* @param Message $iTipMessage
* @param int $lastOccurrence
* @return string
*/
private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
$token = $this->random->generate(60, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
/** @var VEvent $vevent */
$vevent = $iTipMessage->message->VEVENT;
$attendee = $iTipMessage->recipient;
$organizer = $iTipMessage->sender;
$sequence = $iTipMessage->sequence;
$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
$vevent->{'RECURRENCE-ID'}->serialize() : null;
$uid = $vevent->{'UID'};
$query = $this->db->getQueryBuilder();
$query->insert('calendar_invitation_tokens')
->values([
'token' => $query->createNamedParameter($token),
'attendee' => $query->createNamedParameter($attendee),
'organizer' => $query->createNamedParameter($organizer),
'sequence' => $query->createNamedParameter($sequence),
'recurrenceid' => $query->createNamedParameter($recurrenceId),
'expiration' => $query->createNamedParameter($lastOccurrence),
'uid' => $query->createNamedParameter($uid)
])
->execute();
return $token;
}
}

View file

@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Controller;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IDBConnection;
use OCP\IRequest;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Reader;
class InvitationResponseController extends Controller {
/** @var IDBConnection */
private $db;
/** @var ITimeFactory */
private $timeFactory;
/** @var InvitationResponseServer */
private $responseServer;
/**
* InvitationResponseController constructor.
*
* @param string $appName
* @param IRequest $request
* @param IDBConnection $db
* @param ITimeFactory $timeFactory
* @param InvitationResponseServer $responseServer
*/
public function __construct(string $appName, IRequest $request,
IDBConnection $db, ITimeFactory $timeFactory,
InvitationResponseServer $responseServer) {
parent::__construct($appName, $request);
$this->db = $db;
$this->timeFactory = $timeFactory;
$this->responseServer = $responseServer;
// Don't run `$server->exec()`, because we just need access to the
// fully initialized schedule plugin, but we don't want Sabre/DAV
// to actually handle and reply to the request
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @param string $token
* @return TemplateResponse
*/
public function accept(string $token):TemplateResponse {
$row = $this->getTokenInformation($token);
if (!$row) {
return new TemplateResponse($this->appName, 'schedule-response-error', [], 'guest');
}
$iTipMessage = $this->buildITipResponse($row, 'ACCEPTED');
$this->responseServer->handleITipMessage($iTipMessage);
if ($iTipMessage->getScheduleStatus() === '1.2') {
return new TemplateResponse($this->appName, 'schedule-response-success', [], 'guest');
}
return new TemplateResponse($this->appName, 'schedule-response-error', [
'organizer' => $row['organizer'],
], 'guest');
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @param string $token
* @return TemplateResponse
*/
public function decline(string $token):TemplateResponse {
$row = $this->getTokenInformation($token);
if (!$row) {
return new TemplateResponse($this->appName, 'schedule-response-error', [], 'guest');
}
$iTipMessage = $this->buildITipResponse($row, 'DECLINED');
$this->responseServer->handleITipMessage($iTipMessage);
if ($iTipMessage->getScheduleStatus() === '1.2') {
return new TemplateResponse($this->appName, 'schedule-response-success', [], 'guest');
}
return new TemplateResponse($this->appName, 'schedule-response-error', [
'organizer' => $row['organizer'],
], 'guest');
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @param string $token
* @return TemplateResponse
*/
public function options(string $token):TemplateResponse {
return new TemplateResponse($this->appName, 'schedule-response-options', [
'token' => $token
], 'guest');
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @param string $token
*
* @return TemplateResponse
*/
public function processMoreOptionsResult(string $token):TemplateResponse {
$partstat = $this->request->getParam('partStat');
$guests = (int) $this->request->getParam('guests');
$comment = $this->request->getParam('comment');
$row = $this->getTokenInformation($token);
if (!$row || !\in_array($partstat, ['ACCEPTED', 'DECLINED', 'TENTATIVE'])) {
return new TemplateResponse($this->appName, 'schedule-response-error', [], 'guest');
}
$iTipMessage = $this->buildITipResponse($row, $partstat, $guests, $comment);
$this->responseServer->handleITipMessage($iTipMessage);
if ($iTipMessage->getScheduleStatus() === '1.2') {
return new TemplateResponse($this->appName, 'schedule-response-success', [], 'guest');
}
return new TemplateResponse($this->appName, 'schedule-response-error', [
'organizer' => $row['organizer'],
], 'guest');
}
/**
* @param string $token
* @return array|null
*/
private function getTokenInformation(string $token) {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from('calendar_invitation_tokens')
->where($query->expr()->eq('token', $query->createNamedParameter($token)));
$stmt = $query->execute();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if(!$row) {
return null;
}
$currentTime = $this->timeFactory->getTime();
if (((int) $row['expiration']) < $currentTime) {
return null;
}
return $row;
}
/**
* @param array $row
* @param string $partStat participation status of attendee - SEE RFC 5545
* @param int|null $guests
* @param string|null $comment
* @return Message
*/
private function buildITipResponse(array $row, string $partStat, int $guests=null,
string $comment=null):Message {
$iTipMessage = new Message();
$iTipMessage->uid = $row['uid'];
$iTipMessage->component = 'VEVENT';
$iTipMessage->method = 'REPLY';
$iTipMessage->sequence = $row['sequence'];
$iTipMessage->sender = $row['attendee'];
$iTipMessage->recipient = $row['organizer'];
$message = <<<EOF
BEGIN:VCALENDAR
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
VERSION:2.0
BEGIN:VEVENT
ATTENDEE;PARTSTAT=%s:%s
ORGANIZER:%s
UID:%s
SEQUENCE:%s
REQUEST-STATUS:2.0;Success
%sEND:VEVENT
END:VCALENDAR
EOF;
$vObject = Reader::read(vsprintf($message, [
$partStat, $row['attendee'], $row['organizer'],
$row['uid'], $row['sequence'] ?? 0, $row['recurrenceid'] ?? ''
]));
$vEvent = $vObject->{'VEVENT'};
/** @var \Sabre\VObject\Property\ICalendar\CalAddress $attendee */
$attendee = $vEvent->{'ATTENDEE'};
$vEvent->DTSTAMP = date('Ymd\\THis\\Z', $this->timeFactory->getTime());
if ($comment) {
$attendee->add('X-RESPONSE-COMMENT', $comment);
$vEvent->add('COMMENT', $comment);
}
if ($guests) {
$attendee->add('X-NUM-GUESTS', $guests);
}
$iTipMessage->message = $vObject;
return $iTipMessage;
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace OCA\DAV\Migration;
use Doctrine\DBAL\Types\Type;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
/**
* Auto-generated migration step: Please modify to your needs!
*/
class Version1006Date20180619154313 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
* @since 13.0.0
*/
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('calendar_invitation_tokens')) {
$table = $schema->createTable('calendar_invitation_tokens');
$table->addColumn('id', Type::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 11,
'unsigned' => true,
]);
$table->addColumn('uid', Type::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('recurrenceid', Type::STRING, [
'notnull' => false,
'length' => 255,
]);
$table->addColumn('attendee', Type::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('organizer', Type::STRING, [
'notnull' => true,
'length' => 255,
]);
$table->addColumn('sequence', Type::BIGINT, [
'notnull' => false,
'length' => 11,
'unsigned' => true,
]);
$table->addColumn('token', Type::STRING, [
'notnull' => true,
'length' => 60,
]);
$table->addColumn('expiration', Type::BIGINT, [
'notnull' => true,
'length' => 11,
'unsigned' => true,
]);
$table->setPrimaryKey(['id'], 'calendar_invitation_tokens_id_idx');
$table->addIndex(['token'], 'calendar_invitation_tokens_token_idx');
return $schema;
}
}
}

View file

@ -0,0 +1,7 @@
<div class="update">
<p class="message"><?php p($l->t('There was an error updating your attendance status.'));?></p>
<p class="message"><?php p($l->t('Please contact the organizer directly.'));?></p>
<?php if(isset($_['organizer'])): ?>
<p class="message"><a href="<?php p($_['organizer']) ?>"><?php p(substr($_['organizer'], 7)) ?></a></p>
<?php endif; ?>
</div>

View file

@ -0,0 +1,35 @@
<?php
style('dav', 'schedule-response');
//script('dav', 'schedule-response');
?>
<div class="update">
<form action="" method="post">
<fieldset id="partStat">
<h2><?php p($l->t('Are you accepting the invitation?')); ?></h2>
<div id="selectPartStatForm">
<input type="radio" id="partStatAccept" name="partStat" value="ACCEPTED" checked />
<label for="partStatAccept">
<span><?php p($l->t('Accept')); ?></span>
</label>
<input type="radio" id="partStatTentative" name="partStat" value="TENTATIVE" />
<label for="partStatTentative">
<span><?php p($l->t('Tentative')); ?></span>
</label>
<input type="radio" class="declined" id="partStatDeclined" name="partStat" value="DECLINED" />
<label for="partStatDeclined">
<span><?php p($l->t('Decline')); ?></span>
</label>
</div>
</fieldset>
<fieldset id="more_options">
<input type="number" min="0" name="guests" placeholder="Guests" />
<input type="text" name="comment" placeholder="Comment" />
</fieldset>
<fieldset>
<input type="submit" value="<?php p($l->t('Save'));?>">
</fieldset>
</form>
</div>

View file

@ -0,0 +1,4 @@
<div class="update" style="justify-content: space-around; display: flex;">
<span class="icon icon-checkmark-white"></span>
<p class="message"><?php p($l->t('Your attendance was updated successfully.'));?></p>
</div>

View file

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Tests\unit\BackgroundJob;
use OCA\DAV\BackgroundJob\CleanupInvitationTokenJob;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use Test\TestCase;
class CleanupInvitationTokenJobTest extends TestCase {
/** @var IDBConnection | \PHPUnit_Framework_MockObject_MockObject */
private $dbConnection;
/** @var ITimeFactory | \PHPUnit_Framework_MockObject_MockObject */
private $timeFactory;
/** @var \OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob */
private $backgroundJob;
protected function setUp() {
parent::setUp();
$this->dbConnection = $this->createMock(IDBConnection::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->backgroundJob = new CleanupInvitationTokenJob(
$this->dbConnection, $this->timeFactory);
}
public function testRun() {
$this->timeFactory->expects($this->once())
->method('getTime')
->with()
->will($this->returnValue(1337));
$queryBuilder = $this->createMock(IQueryBuilder::class);
$expr = $this->createMock(\OCP\DB\QueryBuilder\IExpressionBuilder::class);
$stmt = $this->createMock(\Doctrine\DBAL\Driver\Statement::class);
$this->dbConnection->expects($this->once())
->method('getQueryBuilder')
->with()
->will($this->returnValue($queryBuilder));
$queryBuilder->method('expr')
->will($this->returnValue($expr));
$queryBuilder->method('createNamedParameter')
->will($this->returnValueMap([
[1337, \PDO::PARAM_STR, null, 'namedParameter1337']
]));
$expr->expects($this->once())
->method('lt')
->with('expiration', 'namedParameter1337')
->will($this->returnValue('LT STATEMENT'));
$this->dbConnection->expects($this->once())
->method('getQueryBuilder')
->with()
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(0))
->method('delete')
->with('calendar_invitation_tokens')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(3))
->method('where')
->with('LT STATEMENT')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(4))
->method('execute')
->with()
->will($this->returnValue($stmt));
$this->backgroundJob->run([]);
}
}

View file

@ -29,8 +29,10 @@ namespace OCA\DAV\Tests\unit\CalDAV\Schedule;
use OC\Mail\Mailer;
use OCA\DAV\CalDAV\Schedule\IMipPlugin;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IURLGenerator;
@ -39,6 +41,7 @@ use OCP\Mail\IAttachment;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
use OCP\Security\ISecureRandom;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\ITip\Message;
use Test\TestCase;
@ -70,13 +73,38 @@ class IMipPluginTest extends TestCase {
$l10nFactory->method('get')->willReturn($l10n);
/** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject $urlGenerator */
$urlGenerator = $this->createMock(IURLGenerator::class);
/** @var IDBConnection | \PHPUnit_Framework_MockObject_MockObject $db */
$db = $this->createMock(IDBConnection::class);
/** @var ISecureRandom | \PHPUnit_Framework_MockObject_MockObject $random */
$random = $this->createMock(ISecureRandom::class);
/** @var Defaults | \PHPUnit_Framework_MockObject_MockObject $defaults */
$defaults = $this->createMock(Defaults::class);
$defaults->expects($this->once())
->method('getName')
->will($this->returnValue('Instance Name 123'));
$plugin = new IMipPlugin($config, $mailer, $logger, $timeFactory, $l10nFactory, $urlGenerator, $defaults, 'user123');
$random->expects($this->once())
->method('generate')
->with(60, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
->will($this->returnValue('random_token'));
$queryBuilder = $this->createMock(IQueryBuilder::class);
$db->expects($this->once())
->method('getQueryBuilder')
->with()
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(0))
->method('insert')
->with('calendar_invitation_tokens')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(8))
->method('values')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(9))
->method('execute');
$plugin = new IMipPlugin($config, $mailer, $logger, $timeFactory, $l10nFactory, $urlGenerator, $defaults, $random, $db, 'user123');
$message = new Message();
$message->method = 'REQUEST';
$message->message = new VCalendar();
@ -128,10 +156,35 @@ class IMipPluginTest extends TestCase {
$l10nFactory->method('get')->willReturn($l10n);
/** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject $urlGenerator */
$urlGenerator = $this->createMock(IURLGenerator::class);
/** @var IDBConnection | \PHPUnit_Framework_MockObject_MockObject $db */
$db = $this->createMock(IDBConnection::class);
/** @var ISecureRandom | \PHPUnit_Framework_MockObject_MockObject $random */
$random = $this->createMock(ISecureRandom::class);
/** @var Defaults | \PHPUnit_Framework_MockObject_MockObject $defaults */
$defaults = $this->createMock(Defaults::class);
$plugin = new IMipPlugin($config, $mailer, $logger, $timeFactory, $l10nFactory, $urlGenerator, $defaults, 'user123');
$random->expects($this->once())
->method('generate')
->with(60, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
->will($this->returnValue('random_token'));
$queryBuilder = $this->createMock(IQueryBuilder::class);
$db->expects($this->once())
->method('getQueryBuilder')
->with()
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(0))
->method('insert')
->with('calendar_invitation_tokens')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(8))
->method('values')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(9))
->method('execute');
$plugin = new IMipPlugin($config, $mailer, $logger, $timeFactory, $l10nFactory, $urlGenerator, $defaults, $random, $db, 'user123');
$message = new Message();
$message->method = 'REQUEST';
$message->message = new VCalendar();
@ -190,10 +243,37 @@ class IMipPluginTest extends TestCase {
$l10nFactory->method('get')->willReturn($l10n);
/** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject $urlGenerator */
$urlGenerator = $this->createMock(IURLGenerator::class);
/** @var IDBConnection | \PHPUnit_Framework_MockObject_MockObject $db */
$db = $this->createMock(IDBConnection::class);
/** @var ISecureRandom | \PHPUnit_Framework_MockObject_MockObject $random */
$random = $this->createMock(ISecureRandom::class);
/** @var Defaults | \PHPUnit_Framework_MockObject_MockObject $defaults */
$defaults = $this->createMock(Defaults::class);
$plugin = new IMipPlugin($config, $mailer, $logger, $timeFactory, $l10nFactory, $urlGenerator, $defaults, 'user123');
if ($expectsMail) {
$random->expects($this->once())
->method('generate')
->with(60, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789')
->will($this->returnValue('random_token'));
$queryBuilder = $this->createMock(IQueryBuilder::class);
$db->expects($this->once())
->method('getQueryBuilder')
->with()
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(0))
->method('insert')
->with('calendar_invitation_tokens')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(8))
->method('values')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(9))
->method('execute');
}
$plugin = new IMipPlugin($config, $mailer, $logger, $timeFactory, $l10nFactory, $urlGenerator, $defaults, $random, $db, 'user123');
$message = new Message();
$message->method = 'REQUEST';
$message->message = new VCalendar();

View file

@ -0,0 +1,455 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Tests\Unit\DAV\Controller;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCA\DAV\CalDAV\Schedule\Plugin;
use OCA\DAV\Controller\InvitationResponseController;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IRequest;
use Sabre\VObject\ITip\Message;
use Test\TestCase;
class InvitationResponseControllerTest extends TestCase {
/** @var InvitationResponseController */
private $controller;
/** @var IDBConnection|\PHPUnit_Framework_MockObject_MockObject */
private $dbConnection;
/** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */
private $request;
/** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */
private $timeFactory;
/** @var InvitationResponseServer|\PHPUnit_Framework_MockObject_MockObject */
private $responseServer;
public function setUp() {
parent::setUp();
$this->dbConnection = $this->createMock(IDBConnection::class);
$this->request = $this->createMock(IRequest::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->responseServer = $this->getMockBuilder(InvitationResponseServer::class)
->disableOriginalConstructor()
->getMock();
$this->controller = new InvitationResponseController(
'appName',
$this->request,
$this->dbConnection,
$this->timeFactory,
$this->responseServer
);
}
public function testAccept() {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
'uid' => 'this-is-the-events-uid',
'recurrenceid' => null,
'attendee' => 'mailto:attendee@foo.bar',
'organizer' => 'mailto:organizer@foo.bar',
'sequence' => null,
'token' => 'TOKEN123',
'expiration' => 420000,
], 1337);
$expected = <<<EOF
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar
ORGANIZER:mailto:organizer@foo.bar
UID:this-is-the-events-uid
SEQUENCE:0
REQUEST-STATUS:2.0;Success
DTSTAMP:19700101T002217Z
END:VEVENT
END:VCALENDAR
EOF;
$expected = preg_replace('~\R~u', "\r\n", $expected);
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
->will($this->returnCallback(function(Message $iTipMessage) use (&$called, $expected) {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
$this->assertEquals('REPLY', $iTipMessage->method);
$this->assertEquals(null, $iTipMessage->sequence);
$this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
$this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
$iTipMessage->scheduleStatus = '1.2;Message delivered locally';
$this->assertEquals($expected, $iTipMessage->message->serialize());
}));
$response = $this->controller->accept('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-success', $response->getTemplateName());
$this->assertEquals([], $response->getParams());
$this->assertTrue($called);
}
public function testAcceptSequence() {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
'uid' => 'this-is-the-events-uid',
'recurrenceid' => null,
'attendee' => 'mailto:attendee@foo.bar',
'organizer' => 'mailto:organizer@foo.bar',
'sequence' => 1337,
'token' => 'TOKEN123',
'expiration' => 420000,
], 1337);
$expected = <<<EOF
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar
ORGANIZER:mailto:organizer@foo.bar
UID:this-is-the-events-uid
SEQUENCE:1337
REQUEST-STATUS:2.0;Success
DTSTAMP:19700101T002217Z
END:VEVENT
END:VCALENDAR
EOF;
$expected = preg_replace('~\R~u', "\r\n", $expected);
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
->will($this->returnCallback(function(Message $iTipMessage) use (&$called, $expected) {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
$this->assertEquals('REPLY', $iTipMessage->method);
$this->assertEquals(1337, $iTipMessage->sequence);
$this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
$this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
$iTipMessage->scheduleStatus = '1.2;Message delivered locally';
$this->assertEquals($expected, $iTipMessage->message->serialize());
}));
$response = $this->controller->accept('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-success', $response->getTemplateName());
$this->assertEquals([], $response->getParams());
$this->assertTrue($called);
}
public function testAcceptRecurrenceId() {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
'uid' => 'this-is-the-events-uid',
'recurrenceid' => "RECURRENCE-ID;TZID=Europe/Berlin:20180726T150000\n",
'attendee' => 'mailto:attendee@foo.bar',
'organizer' => 'mailto:organizer@foo.bar',
'sequence' => null,
'token' => 'TOKEN123',
'expiration' => 420000,
], 1337);
$expected = <<<EOF
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED:mailto:attendee@foo.bar
ORGANIZER:mailto:organizer@foo.bar
UID:this-is-the-events-uid
SEQUENCE:0
REQUEST-STATUS:2.0;Success
RECURRENCE-ID;TZID=Europe/Berlin:20180726T150000
DTSTAMP:19700101T002217Z
END:VEVENT
END:VCALENDAR
EOF;
$expected = preg_replace('~\R~u', "\r\n", $expected);
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
->will($this->returnCallback(function(Message $iTipMessage) use (&$called, $expected) {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
$this->assertEquals('REPLY', $iTipMessage->method);
$this->assertEquals(0, $iTipMessage->sequence);
$this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
$this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
$iTipMessage->scheduleStatus = '1.2;Message delivered locally';
$this->assertEquals($expected, $iTipMessage->message->serialize());
}));
$response = $this->controller->accept('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-success', $response->getTemplateName());
$this->assertEquals([], $response->getParams());
$this->assertTrue($called);
}
public function testAcceptTokenNotFound() {
$this->buildQueryExpects('TOKEN123', null, 1337);
$response = $this->controller->accept('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-error', $response->getTemplateName());
$this->assertEquals([], $response->getParams());
}
public function testAcceptExpiredToken() {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
'uid' => 'this-is-the-events-uid',
'recurrenceid' => null,
'attendee' => 'mailto:attendee@foo.bar',
'organizer' => 'mailto:organizer@foo.bar',
'sequence' => null,
'token' => 'TOKEN123',
'expiration' => 42,
], 1337);
$response = $this->controller->accept('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-error', $response->getTemplateName());
$this->assertEquals([], $response->getParams());
}
public function testDecline() {
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
'uid' => 'this-is-the-events-uid',
'recurrenceid' => null,
'attendee' => 'mailto:attendee@foo.bar',
'organizer' => 'mailto:organizer@foo.bar',
'sequence' => null,
'token' => 'TOKEN123',
'expiration' => 420000,
], 1337);
$expected = <<<EOF
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
BEGIN:VEVENT
ATTENDEE;PARTSTAT=DECLINED:mailto:attendee@foo.bar
ORGANIZER:mailto:organizer@foo.bar
UID:this-is-the-events-uid
SEQUENCE:0
REQUEST-STATUS:2.0;Success
DTSTAMP:19700101T002217Z
END:VEVENT
END:VCALENDAR
EOF;
$expected = preg_replace('~\R~u', "\r\n", $expected);
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
->will($this->returnCallback(function(Message $iTipMessage) use (&$called, $expected) {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
$this->assertEquals('REPLY', $iTipMessage->method);
$this->assertEquals(null, $iTipMessage->sequence);
$this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
$this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
$iTipMessage->scheduleStatus = '1.2;Message delivered locally';
$this->assertEquals($expected, $iTipMessage->message->serialize());
}));
$response = $this->controller->decline('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-success', $response->getTemplateName());
$this->assertEquals([], $response->getParams());
$this->assertTrue($called);
}
public function testOptions() {
$response = $this->controller->options('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-options', $response->getTemplateName());
$this->assertEquals(['token' => 'TOKEN123'], $response->getParams());
}
public function testProcessMoreOptionsResult() {
$this->request->expects($this->at(0))
->method('getParam')
->with('partStat')
->will($this->returnValue('TENTATIVE'));
$this->request->expects($this->at(1))
->method('getParam')
->with('guests')
->will($this->returnValue('7'));
$this->request->expects($this->at(2))
->method('getParam')
->with('comment')
->will($this->returnValue('Foo bar Bli blub'));
$this->buildQueryExpects('TOKEN123', [
'id' => 0,
'uid' => 'this-is-the-events-uid',
'recurrenceid' => null,
'attendee' => 'mailto:attendee@foo.bar',
'organizer' => 'mailto:organizer@foo.bar',
'sequence' => null,
'token' => 'TOKEN123',
'expiration' => 420000,
], 1337);
$expected = <<<EOF
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
BEGIN:VEVENT
ATTENDEE;PARTSTAT=TENTATIVE;X-RESPONSE-COMMENT=Foo bar Bli blub;X-NUM-GUEST
S=7:mailto:attendee@foo.bar
ORGANIZER:mailto:organizer@foo.bar
UID:this-is-the-events-uid
SEQUENCE:0
REQUEST-STATUS:2.0;Success
DTSTAMP:19700101T002217Z
COMMENT:Foo bar Bli blub
END:VEVENT
END:VCALENDAR
EOF;
$expected = preg_replace('~\R~u', "\r\n", $expected);
$called = false;
$this->responseServer->expects($this->once())
->method('handleITipMessage')
->will($this->returnCallback(function(Message $iTipMessage) use (&$called, $expected) {
$called = true;
$this->assertEquals('this-is-the-events-uid', $iTipMessage->uid);
$this->assertEquals('VEVENT', $iTipMessage->component);
$this->assertEquals('REPLY', $iTipMessage->method);
$this->assertEquals(null, $iTipMessage->sequence);
$this->assertEquals('mailto:attendee@foo.bar', $iTipMessage->sender);
$this->assertEquals('mailto:organizer@foo.bar', $iTipMessage->recipient);
$iTipMessage->scheduleStatus = '1.2;Message delivered locally';
$this->assertEquals($expected, $iTipMessage->message->serialize());
}));
$response = $this->controller->processMoreOptionsResult('TOKEN123');
$this->assertInstanceOf(TemplateResponse::class, $response);
$this->assertEquals('schedule-response-success', $response->getTemplateName());
$this->assertEquals([], $response->getParams());
$this->assertTrue($called);
}
private function buildQueryExpects($token, $return, $time) {
$queryBuilder = $this->createMock(IQueryBuilder::class);
$stmt = $this->createMock(\Doctrine\DBAL\Driver\Statement::class);
$expr = $this->createMock(\OCP\DB\QueryBuilder\IExpressionBuilder::class);
$this->dbConnection->expects($this->once())
->method('getQueryBuilder')
->with()
->will($this->returnValue($queryBuilder));
$queryBuilder->method('expr')
->will($this->returnValue($expr));
$queryBuilder->method('createNamedParameter')
->will($this->returnValueMap([
[$token, \PDO::PARAM_STR, null, 'namedParameterToken']
]));
$stmt->expects($this->once())
->method('fetch')
->with(\PDO::FETCH_ASSOC)
->will($this->returnValue($return));
$expr->expects($this->once())
->method('eq')
->with('token', 'namedParameterToken')
->will($this->returnValue('EQ STATEMENT'));
$this->dbConnection->expects($this->once())
->method('getQueryBuilder')
->with()
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(0))
->method('select')
->with('*')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(1))
->method('from')
->with('calendar_invitation_tokens')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(4))
->method('where')
->with('EQ STATEMENT')
->will($this->returnValue($queryBuilder));
$queryBuilder->expects($this->at(5))
->method('execute')
->with()
->will($this->returnValue($stmt));
$this->timeFactory->method('getTime')
->will($this->returnValue($time));
}
}