aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav
diff options
context:
space:
mode:
authorGeorg Ehrke <developer@georgehrke.com>2018-06-19 21:01:14 +0200
committerRoeland Jago Douma <roeland@famdouma.nl>2018-06-29 10:44:44 +0200
commit4aa4e4080c27356792b6eeab1f2f2b36ad485a05 (patch)
tree07197c21582d4e74cacce3704eaa5a67b5fb89bb /apps/dav
parent3ff3141a1e4c9482ddaa68e13f545eb7e62ff9b7 (diff)
downloadnextcloud-server-4aa4e4080c27356792b6eeab1f2f2b36ad485a05.tar.gz
nextcloud-server-4aa4e4080c27356792b6eeab1f2f2b36ad485a05.zip
Include accept / decline links in CalDAV invitation emails
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
Diffstat (limited to 'apps/dav')
-rw-r--r--apps/dav/appinfo/info.xml1
-rw-r--r--apps/dav/appinfo/routes.php4
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php4
-rw-r--r--apps/dav/composer/composer/autoload_static.php4
-rw-r--r--apps/dav/css/schedule-response.css78
-rw-r--r--apps/dav/js/schedule-response.js3
-rw-r--r--apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php53
-rw-r--r--apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php118
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php94
-rw-r--r--apps/dav/lib/Controller/InvitationResponseController.php236
-rw-r--r--apps/dav/lib/Migration/Version1006Date20180619154313.php71
-rw-r--r--apps/dav/templates/schedule-response-error.php7
-rw-r--r--apps/dav/templates/schedule-response-options.php35
-rw-r--r--apps/dav/templates/schedule-response-success.php4
-rw-r--r--apps/dav/tests/unit/BackgroundJob/CleanupInvitationTokenJobTest.php100
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php86
-rw-r--r--apps/dav/tests/unit/Controller/InvitationResponseControllerTest.php455
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