diff options
17 files changed, 1344 insertions, 9 deletions
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index d1f9a823fd7..f4b1cb5b5c2 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -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> diff --git a/apps/dav/appinfo/routes.php b/apps/dav/appinfo/routes.php index 2aaeda98964..a7d9e2ec33c 100644 --- a/apps/dav/appinfo/routes.php +++ b/apps/dav/appinfo/routes.php @@ -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'], diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 8ed5e068e2d..8b266c156f3 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -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', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index bdedf1a4404..09eb4d257cc 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -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', diff --git a/apps/dav/css/schedule-response.css b/apps/dav/css/schedule-response.css new file mode 100644 index 00000000000..789ea16df7a --- /dev/null +++ b/apps/dav/css/schedule-response.css @@ -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 +}
\ No newline at end of file diff --git a/apps/dav/js/schedule-response.js b/apps/dav/js/schedule-response.js new file mode 100644 index 00000000000..b2514b1ba82 --- /dev/null +++ b/apps/dav/js/schedule-response.js @@ -0,0 +1,3 @@ +// $(document).ready(function() { +// $('#selectPartStatForm').buttonset(); +// });
\ No newline at end of file diff --git a/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php b/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php new file mode 100644 index 00000000000..e855b57ee9f --- /dev/null +++ b/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php @@ -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(); + } +} diff --git a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php new file mode 100644 index 00000000000..61ead99ce12 --- /dev/null +++ b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php @@ -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); + } +}
\ No newline at end of file diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index 85973a8be12..40b4ee355f3 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -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; } @@ -460,6 +477,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; + } } diff --git a/apps/dav/lib/Controller/InvitationResponseController.php b/apps/dav/lib/Controller/InvitationResponseController.php new file mode 100644 index 00000000000..08298a29f0f --- /dev/null +++ b/apps/dav/lib/Controller/InvitationResponseController.php @@ -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; + } +} diff --git a/apps/dav/lib/Migration/Version1006Date20180619154313.php b/apps/dav/lib/Migration/Version1006Date20180619154313.php new file mode 100644 index 00000000000..8fb82ffed67 --- /dev/null +++ b/apps/dav/lib/Migration/Version1006Date20180619154313.php @@ -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; + } + } +} diff --git a/apps/dav/templates/schedule-response-error.php b/apps/dav/templates/schedule-response-error.php new file mode 100644 index 00000000000..c65875f3b0b --- /dev/null +++ b/apps/dav/templates/schedule-response-error.php @@ -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> diff --git a/apps/dav/templates/schedule-response-options.php b/apps/dav/templates/schedule-response-options.php new file mode 100644 index 00000000000..da95454e4f5 --- /dev/null +++ b/apps/dav/templates/schedule-response-options.php @@ -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> diff --git a/apps/dav/templates/schedule-response-success.php b/apps/dav/templates/schedule-response-success.php new file mode 100644 index 00000000000..f60cb1e0fa9 --- /dev/null +++ b/apps/dav/templates/schedule-response-success.php @@ -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> diff --git a/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php b/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php new file mode 100644 index 00000000000..3e69230f441 --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php @@ -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([]); + } +}
\ No newline at end of file diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php index 3f89002ab98..2fefe8dd06a 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php @@ -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(); diff --git a/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php new file mode 100644 index 00000000000..333737e2262 --- /dev/null +++ b/apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php @@ -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)); + } +}
\ No newline at end of file |