diff --git a/3rdparty b/3rdparty index ef289bc27e..49ccfbb286 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit ef289bc27eae0cdfc3f74f419ace8dda8dd84ef0 +Subproject commit 49ccfbb28661b9ef7743c1725cd2571259215929 diff --git a/apps/dav/appinfo/app.php b/apps/dav/appinfo/app.php index 70d01088be..dd9e0e9c09 100644 --- a/apps/dav/appinfo/app.php +++ b/apps/dav/appinfo/app.php @@ -108,3 +108,6 @@ $calendarManager->register(function() use ($calendarManager, $app) { $app->setupCalendarProvider($calendarManager, $user->getUID()); } }); + +$app->registerNotifier(); +$app->registerCalendarReminders(); diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index dc90ac5818..6b593b97b7 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ WebDAV WebDAV endpoint WebDAV endpoint - 1.11.1 + 1.13.0 agpl owncloud.org DAV @@ -23,6 +23,7 @@ OCA\DAV\BackgroundJob\CleanupDirectLinksJob OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob OCA\DAV\BackgroundJob\CleanupInvitationTokenJob + OCA\DAV\BackgroundJob\EventReminderJob @@ -32,6 +33,7 @@ OCA\DAV\Migration\CalDAVRemoveEmptyValue OCA\DAV\Migration\BuildCalendarSearchIndex OCA\DAV\Migration\RefreshWebcalJobRegistrar + OCA\DAV\Migration\RegisterBuildReminderIndexBackgroundJob OCA\DAV\Migration\RemoveOrphanEventsAndContacts OCA\DAV\Migration\RemoveClassifiedEventActivity @@ -45,6 +47,7 @@ OCA\DAV\Command\CreateCalendar OCA\DAV\Command\MoveCalendar OCA\DAV\Command\ListCalendars + OCA\DAV\Command\SendEventReminders OCA\DAV\Command\SyncBirthdayCalendar OCA\DAV\Command\SyncSystemAddressBook OCA\DAV\Command\RemoveInvalidShares diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 694231eebd..1084430719 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -11,8 +11,10 @@ return array( 'OCA\\DAV\\Avatars\\AvatarHome' => $baseDir . '/../lib/Avatars/AvatarHome.php', 'OCA\\DAV\\Avatars\\AvatarNode' => $baseDir . '/../lib/Avatars/AvatarNode.php', 'OCA\\DAV\\Avatars\\RootCollection' => $baseDir . '/../lib/Avatars/RootCollection.php', + 'OCA\\DAV\\BackgroundJob\\BuildReminderIndexBackgroundJob' => $baseDir . '/../lib/BackgroundJob/BuildReminderIndexBackgroundJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => $baseDir . '/../lib/BackgroundJob/CleanupDirectLinksJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', + 'OCA\\DAV\\BackgroundJob\\EventReminderJob' => $baseDir . '/../lib/BackgroundJob/EventReminderJob.php', 'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php', 'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => $baseDir . '/../lib/BackgroundJob/RefreshWebcalJob.php', 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => $baseDir . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', @@ -51,6 +53,17 @@ return array( 'OCA\\DAV\\CalDAV\\PublicCalendarRoot' => $baseDir . '/../lib/CalDAV/PublicCalendarRoot.php', 'OCA\\DAV\\CalDAV\\Publishing\\PublishPlugin' => $baseDir . '/../lib/CalDAV/Publishing/PublishPlugin.php', 'OCA\\DAV\\CalDAV\\Publishing\\Xml\\Publisher' => $baseDir . '/../lib/CalDAV/Publishing/Xml/Publisher.php', + 'OCA\\DAV\\CalDAV\\Reminder\\Backend' => $baseDir . '/../lib/CalDAV/Reminder/Backend.php', + 'OCA\\DAV\\CalDAV\\Reminder\\INotificationProvider' => $baseDir . '/../lib/CalDAV/Reminder/INotificationProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProviderManager' => $baseDir . '/../lib/CalDAV/Reminder/NotificationProviderManager.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\AbstractProvider' => $baseDir . '/../lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\AudioProvider' => $baseDir . '/../lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\EmailProvider' => $baseDir . '/../lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\ProviderNotAvailableException' => $baseDir . '/../lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\PushProvider' => $baseDir . '/../lib/CalDAV/Reminder/NotificationProvider/PushProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationTypeDoesNotExistException' => $baseDir . '/../lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php', + 'OCA\\DAV\\CalDAV\\Reminder\\Notifier' => $baseDir . '/../lib/CalDAV/Reminder/Notifier.php', + 'OCA\\DAV\\CalDAV\\Reminder\\ReminderService' => $baseDir . '/../lib/CalDAV/Reminder/ReminderService.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\AbstractPrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\ResourcePrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => $baseDir . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php', @@ -86,6 +99,7 @@ return array( 'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php', 'OCA\\DAV\\Command\\MoveCalendar' => $baseDir . '/../lib/Command/MoveCalendar.php', 'OCA\\DAV\\Command\\RemoveInvalidShares' => $baseDir . '/../lib/Command/RemoveInvalidShares.php', + 'OCA\\DAV\\Command\\SendEventReminders' => $baseDir . '/../lib/Command/SendEventReminders.php', 'OCA\\DAV\\Command\\SyncBirthdayCalendar' => $baseDir . '/../lib/Command/SyncBirthdayCalendar.php', 'OCA\\DAV\\Command\\SyncSystemAddressBook' => $baseDir . '/../lib/Command/SyncSystemAddressBook.php', 'OCA\\DAV\\Comments\\CommentNode' => $baseDir . '/../lib/Comments/CommentNode.php', @@ -166,6 +180,7 @@ return array( 'OCA\\DAV\\Migration\\FixBirthdayCalendarComponent' => $baseDir . '/../lib/Migration/FixBirthdayCalendarComponent.php', 'OCA\\DAV\\Migration\\RefreshWebcalJobRegistrar' => $baseDir . '/../lib/Migration/RefreshWebcalJobRegistrar.php', 'OCA\\DAV\\Migration\\RegenerateBirthdayCalendars' => $baseDir . '/../lib/Migration/RegenerateBirthdayCalendars.php', + 'OCA\\DAV\\Migration\\RegisterBuildReminderIndexBackgroundJob' => $baseDir . '/../lib/Migration/RegisterBuildReminderIndexBackgroundJob.php', 'OCA\\DAV\\Migration\\RemoveClassifiedEventActivity' => $baseDir . '/../lib/Migration/RemoveClassifiedEventActivity.php', 'OCA\\DAV\\Migration\\RemoveOrphanEventsAndContacts' => $baseDir . '/../lib/Migration/RemoveOrphanEventsAndContacts.php', 'OCA\\DAV\\Migration\\Version1004Date20170825134824' => $baseDir . '/../lib/Migration/Version1004Date20170825134824.php', @@ -184,6 +199,7 @@ return array( 'OCA\\DAV\\Migration\\Version1008Date20181114084440' => $baseDir . '/../lib/Migration/Version1008Date20181114084440.php', 'OCA\\DAV\\Migration\\Version1011Date20190725113607' => $baseDir . '/../lib/Migration/Version1011Date20190725113607.php', 'OCA\\DAV\\Migration\\Version1011Date20190806104428' => $baseDir . '/../lib/Migration/Version1011Date20190806104428.php', + 'OCA\\DAV\\Migration\\Version1012Date20190808122342' => $baseDir . '/../lib/Migration/Version1012Date20190808122342.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', 'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 6f10491607..b3f5688166 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -26,8 +26,10 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Avatars\\AvatarHome' => __DIR__ . '/..' . '/../lib/Avatars/AvatarHome.php', 'OCA\\DAV\\Avatars\\AvatarNode' => __DIR__ . '/..' . '/../lib/Avatars/AvatarNode.php', 'OCA\\DAV\\Avatars\\RootCollection' => __DIR__ . '/..' . '/../lib/Avatars/RootCollection.php', + 'OCA\\DAV\\BackgroundJob\\BuildReminderIndexBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/BuildReminderIndexBackgroundJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectLinksJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', + 'OCA\\DAV\\BackgroundJob\\EventReminderJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/EventReminderJob.php', 'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php', 'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/RefreshWebcalJob.php', 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', @@ -66,6 +68,17 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\PublicCalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/PublicCalendarRoot.php', 'OCA\\DAV\\CalDAV\\Publishing\\PublishPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Publishing/PublishPlugin.php', 'OCA\\DAV\\CalDAV\\Publishing\\Xml\\Publisher' => __DIR__ . '/..' . '/../lib/CalDAV/Publishing/Xml/Publisher.php', + 'OCA\\DAV\\CalDAV\\Reminder\\Backend' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/Backend.php', + 'OCA\\DAV\\CalDAV\\Reminder\\INotificationProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/INotificationProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProviderManager' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/NotificationProviderManager.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\AbstractProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\AudioProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\EmailProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\ProviderNotAvailableException' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationProvider\\PushProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/NotificationProvider/PushProvider.php', + 'OCA\\DAV\\CalDAV\\Reminder\\NotificationTypeDoesNotExistException' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php', + 'OCA\\DAV\\CalDAV\\Reminder\\Notifier' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/Notifier.php', + 'OCA\\DAV\\CalDAV\\Reminder\\ReminderService' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/ReminderService.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\AbstractPrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\ResourcePrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php', 'OCA\\DAV\\CalDAV\\ResourceBooking\\RoomPrincipalBackend' => __DIR__ . '/..' . '/../lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php', @@ -101,6 +114,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php', 'OCA\\DAV\\Command\\MoveCalendar' => __DIR__ . '/..' . '/../lib/Command/MoveCalendar.php', 'OCA\\DAV\\Command\\RemoveInvalidShares' => __DIR__ . '/..' . '/../lib/Command/RemoveInvalidShares.php', + 'OCA\\DAV\\Command\\SendEventReminders' => __DIR__ . '/..' . '/../lib/Command/SendEventReminders.php', 'OCA\\DAV\\Command\\SyncBirthdayCalendar' => __DIR__ . '/..' . '/../lib/Command/SyncBirthdayCalendar.php', 'OCA\\DAV\\Command\\SyncSystemAddressBook' => __DIR__ . '/..' . '/../lib/Command/SyncSystemAddressBook.php', 'OCA\\DAV\\Comments\\CommentNode' => __DIR__ . '/..' . '/../lib/Comments/CommentNode.php', @@ -181,6 +195,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\FixBirthdayCalendarComponent' => __DIR__ . '/..' . '/../lib/Migration/FixBirthdayCalendarComponent.php', 'OCA\\DAV\\Migration\\RefreshWebcalJobRegistrar' => __DIR__ . '/..' . '/../lib/Migration/RefreshWebcalJobRegistrar.php', 'OCA\\DAV\\Migration\\RegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/Migration/RegenerateBirthdayCalendars.php', + 'OCA\\DAV\\Migration\\RegisterBuildReminderIndexBackgroundJob' => __DIR__ . '/..' . '/../lib/Migration/RegisterBuildReminderIndexBackgroundJob.php', 'OCA\\DAV\\Migration\\RemoveClassifiedEventActivity' => __DIR__ . '/..' . '/../lib/Migration/RemoveClassifiedEventActivity.php', 'OCA\\DAV\\Migration\\RemoveOrphanEventsAndContacts' => __DIR__ . '/..' . '/../lib/Migration/RemoveOrphanEventsAndContacts.php', 'OCA\\DAV\\Migration\\Version1004Date20170825134824' => __DIR__ . '/..' . '/../lib/Migration/Version1004Date20170825134824.php', @@ -199,6 +214,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1008Date20181114084440' => __DIR__ . '/..' . '/../lib/Migration/Version1008Date20181114084440.php', 'OCA\\DAV\\Migration\\Version1011Date20190725113607' => __DIR__ . '/..' . '/../lib/Migration/Version1011Date20190725113607.php', 'OCA\\DAV\\Migration\\Version1011Date20190806104428' => __DIR__ . '/..' . '/../lib/Migration/Version1011Date20190806104428.php', + 'OCA\\DAV\\Migration\\Version1012Date20190808122342' => __DIR__ . '/..' . '/../lib/Migration/Version1012Date20190808122342.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', 'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php', diff --git a/apps/dav/js/settings-admin-caldav.js b/apps/dav/js/settings-admin-caldav.js index 1a40c208df..ad30ba6ad3 100644 --- a/apps/dav/js/settings-admin-caldav.js +++ b/apps/dav/js/settings-admin-caldav.js @@ -36,3 +36,9 @@ $('#caldavGenerateBirthdayCalendar').change(function() { $.post(OC.generateUrl('/apps/dav/disableBirthdayCalendar')); } }); + +$('#caldavSendRemindersNotifications').change(function() { + var val = $(this)[0].checked; + + OCP.AppConfig.setValue('dav', 'sendEventReminders', val ? 'yes' : 'no'); +}); diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 41570ee744..80e9dea882 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -30,6 +30,13 @@ use OCA\DAV\CalDAV\Activity\Backend; use OCA\DAV\CalDAV\Activity\Provider\Event; use OCA\DAV\CalDAV\BirthdayService; use OCA\DAV\CalDAV\CalendarManager; +use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProviderManager; +use OCA\DAV\CalDAV\Reminder\Notifier; +use OCA\DAV\CalDAV\Reminder\ReminderService; use OCA\DAV\Capabilities; use OCA\DAV\CardDAV\ContactsManager; use OCA\DAV\CardDAV\PhotoCache; @@ -43,6 +50,8 @@ use Symfony\Component\EventDispatcher\GenericEvent; class Application extends App { + const APP_ID = 'dav'; + /** * Application constructor. */ @@ -109,8 +118,7 @@ class Application extends App { } }); - // carddav/caldav sync event setup - $listener = function($event) { + $birthdayListener = function ($event) { if ($event instanceof GenericEvent) { /** @var BirthdayService $b */ $b = $this->getContainer()->query(BirthdayService::class); @@ -122,9 +130,9 @@ class Application extends App { } }; - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::createCard', $listener); - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $listener); - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', function($event) { + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::createCard', $birthdayListener); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $birthdayListener); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', function ($event) { if ($event instanceof GenericEvent) { /** @var BirthdayService $b */ $b = $this->getContainer()->query(BirthdayService::class); @@ -177,6 +185,11 @@ class Application extends App { $event->getArgument('calendarData'), $event->getArgument('shares') ); + + $reminderBackend = $this->getContainer()->query(ReminderBackend::class); + $reminderBackend->cleanRemindersForCalendar( + $event->getArgument('calendarId') + ); }); $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::updateShares', function(GenericEvent $event) { /** @var Backend $backend */ @@ -187,6 +200,8 @@ class Application extends App { $event->getArgument('add'), $event->getArgument('remove') ); + + // Here we should recalculate if reminders should be sent to new or old sharees }); $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', function(GenericEvent $event) { @@ -214,6 +229,14 @@ class Application extends App { $event->getArgument('shares'), $event->getArgument('objectData') ); + + /** @var ReminderService $reminderBackend */ + $reminderService = $this->getContainer()->query(ReminderService::class); + + $reminderService->onTouchCalendarObject( + $eventName, + $event->getArgument('objectData') + ); }; $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', $listener); $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', $listener); @@ -224,4 +247,23 @@ class Application extends App { return $this->getContainer()->query(SyncService::class); } + public function registerNotifier():void { + $this->getContainer() + ->getServer() + ->getNotificationManager() + ->registerNotifierService(Notifier::class); + } + + public function registerCalendarReminders():void { + try { + /** @var NotificationProviderManager $notificationProviderManager */ + $notificationProviderManager = $this->getContainer()->query(NotificationProviderManager::class); + $notificationProviderManager->registerProvider(AudioProvider::class); + $notificationProviderManager->registerProvider(EmailProvider::class); + $notificationProviderManager->registerProvider(PushProvider::class); + } catch(\Exception $ex) { + $this->getContainer()->getServer()->getLogger()->logException($ex); + } + } + } diff --git a/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php b/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php new file mode 100644 index 0000000000..15c52cd852 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php @@ -0,0 +1,134 @@ + + * + * @author Georg Ehrke + * + * @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 . + * + */ +namespace OCA\DAV\BackgroundJob; + +use OCP\BackgroundJob\QueuedJob; +use OCA\DAV\CalDAV\Reminder\ReminderService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\IDBConnection; +use OCP\ILogger; + +/** + * Class BuildReminderIndexBackgroundJob + * + * @package OCA\DAV\BackgroundJob + */ +class BuildReminderIndexBackgroundJob extends QueuedJob { + + /** @var IDBConnection */ + private $db; + + /** @var ReminderService */ + private $reminderService; + + /** @var ILogger */ + private $logger; + + /** @var IJobList */ + private $jobList; + + /** @var ITimeFactory */ + private $timeFactory; + + /** + * BuildReminderIndexBackgroundJob constructor. + * + * @param IDBConnection $db + * @param ReminderService $reminderService + * @param ILogger $logger + * @param IJobList $jobList + * @param ITimeFactory $timeFactory + */ + public function __construct(IDBConnection $db, + ReminderService $reminderService, + ILogger $logger, + IJobList $jobList, + ITimeFactory $timeFactory) { + $this->db = $db; + $this->reminderService = $reminderService; + $this->logger = $logger; + $this->jobList = $jobList; + $this->timeFactory = $timeFactory; + } + + /** + * @param $arguments + */ + public function run($arguments) { + $offset = (int) $arguments['offset']; + $stopAt = (int) $arguments['stopAt']; + + $this->logger->info('Building calendar reminder index (' . $offset .'/' . $stopAt . ')'); + + $offset = $this->buildIndex($offset, $stopAt); + + if ($offset >= $stopAt) { + $this->logger->info('Building calendar reminder index done'); + } else { + $this->jobList->add(self::class, [ + 'offset' => $offset, + 'stopAt' => $stopAt + ]); + $this->logger->info('Scheduled a new BuildReminderIndexBackgroundJob with offset ' . $offset); + } + } + + /** + * @param int $offset + * @param int $stopAt + * @return int + */ + private function buildIndex(int $offset, int $stopAt):int { + $startTime = $this->timeFactory->getTime(); + + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from('calendarobjects') + ->where($query->expr()->lte('id', $query->createNamedParameter($stopAt))) + ->andWhere($query->expr()->gt('id', $query->createNamedParameter($offset))) + ->orderBy('id', 'ASC'); + + $stmt = $query->execute(); + while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $offset = $row['id']; + if (is_resource($row['calendardata'])) { + $row['calendardata'] = stream_get_contents($row['calendardata']); + } + $row['component'] = $row['componenttype']; + + try { + $this->reminderService->onTouchCalendarObject('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', $row); + } catch(\Exception $ex) { + $this->logger->logException($ex); + } + + if (($this->timeFactory->getTime() - $startTime) > 15) { + return $offset; + } + } + + return $stopAt; + } +} \ No newline at end of file diff --git a/apps/dav/lib/BackgroundJob/EventReminderJob.php b/apps/dav/lib/BackgroundJob/EventReminderJob.php new file mode 100644 index 0000000000..dfa76ffe16 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/EventReminderJob.php @@ -0,0 +1,64 @@ + + * + * @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 + * + */ +namespace OCA\DAV\BackgroundJob; + +use OC\BackgroundJob\TimedJob; +use OCA\DAV\CalDAV\Reminder\ReminderService; +use OCP\IConfig; + +class EventReminderJob extends TimedJob { + + /** @var ReminderService */ + private $reminderService; + + /** @var IConfig */ + private $config; + + /** + * EventReminderJob constructor. + * + * @param ReminderService $reminderService + * @param IConfig $config + */ + public function __construct(ReminderService $reminderService, IConfig $config) { + $this->reminderService = $reminderService; + $this->config = $config; + /** Run every 5 minutes */ + $this->setInterval(5); + } + + /** + * @param $arg + * @throws \OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException + * @throws \OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException + * @throws \OC\User\NoUserException + */ + public function run($arg):void { + if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') { + return; + } + + if ($this->config->getAppValue('dav', 'sendEventRemindersMode', 'backgroundjob') !== 'backgroundjob') { + return; + } + + $this->reminderService->processReminders(); + } +} diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 91281dc0cb..62d3909ce3 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -1135,7 +1135,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription */ function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType=self::CALENDAR_TYPE_CALENDAR) { $extraData = $this->getDenormalizedData($calendarData); - $query = $this->db->getQueryBuilder(); $query->update('calendarobjects') ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) diff --git a/apps/dav/lib/CalDAV/Reminder/Backend.php b/apps/dav/lib/CalDAV/Reminder/Backend.php new file mode 100644 index 0000000000..b3cc013fb3 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/Backend.php @@ -0,0 +1,219 @@ + + * @author Georg Ehrke + * + * @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 . + * + */ +namespace OCA\DAV\CalDAV\Reminder; + +use OCP\IDBConnection; +use OCP\AppFramework\Utility\ITimeFactory; + +/** + * Class Backend + * + * @package OCA\DAV\CalDAV\Reminder + */ +class Backend { + + /** @var IDBConnection */ + protected $db; + + /** @var ITimeFactory */ + private $timeFactory; + + /** + * Backend constructor. + * + * @param IDBConnection $db + * @param ITimeFactory $timeFactory + */ + public function __construct(IDBConnection $db, + ITimeFactory $timeFactory) { + $this->db = $db; + $this->timeFactory = $timeFactory; + } + + /** + * Get all reminders with a notification date before now + * + * @return array + * @throws \Exception + */ + public function getRemindersToProcess():array { + $query = $this->db->getQueryBuilder(); + $query->select(['cr.*', 'co.calendardata', 'c.displayname', 'c.principaluri']) + ->from('calendar_reminders', 'cr') + ->where($query->expr()->lte('cr.notification_date', $query->createNamedParameter($this->timeFactory->getTime()))) + ->leftJoin('cr', 'calendarobjects', 'co', $query->expr()->eq('cr.object_id', 'co.id')) + ->leftJoin('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id')); + $stmt = $query->execute(); + + return array_map( + [$this, 'fixRowTyping'], + $stmt->fetchAll() + ); + } + + /** + * Get all scheduled reminders for an event + * + * @param int $objectId + * @return array + */ + public function getAllScheduledRemindersForEvent(int $objectId):array { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from('calendar_reminders') + ->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId))); + $stmt = $query->execute(); + + return array_map( + [$this, 'fixRowTyping'], + $stmt->fetchAll() + ); + } + + /** + * Insert a new reminder into the database + * + * @param int $calendarId + * @param int $objectId + * @param string $uid + * @param bool $isRecurring + * @param int $recurrenceId + * @param bool $isRecurrenceException + * @param string $eventHash + * @param string $alarmHash + * @param string $type + * @param bool $isRelative + * @param int $notificationDate + * @param bool $isRepeatBased + * @return int The insert id + */ + public function insertReminder(int $calendarId, + int $objectId, + string $uid, + bool $isRecurring, + int $recurrenceId, + bool $isRecurrenceException, + string $eventHash, + string $alarmHash, + string $type, + bool $isRelative, + int $notificationDate, + bool $isRepeatBased):int { + $query = $this->db->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter($calendarId), + 'object_id' => $query->createNamedParameter($objectId), + 'uid' => $query->createNamedParameter($uid), + 'is_recurring' => $query->createNamedParameter($isRecurring ? 1 : 0), + 'recurrence_id' => $query->createNamedParameter($recurrenceId), + 'is_recurrence_exception' => $query->createNamedParameter($isRecurrenceException ? 1 : 0), + 'event_hash' => $query->createNamedParameter($eventHash), + 'alarm_hash' => $query->createNamedParameter($alarmHash), + 'type' => $query->createNamedParameter($type), + 'is_relative' => $query->createNamedParameter($isRelative ? 1 : 0), + 'notification_date' => $query->createNamedParameter($notificationDate), + 'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0), + ]) + ->execute(); + + return $query->getLastInsertId(); + } + + /** + * Sets a new notificationDate on an existing reminder + * + * @param int $reminderId + * @param int $newNotificationDate + */ + public function updateReminder(int $reminderId, + int $newNotificationDate):void { + $query = $this->db->getQueryBuilder(); + $query->update('calendar_reminders') + ->set('notification_date', $query->createNamedParameter($newNotificationDate)) + ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) + ->execute(); + } + + /** + * Remove a reminder by it's id + * + * @param integer $reminderId + * @return void + */ + public function removeReminder(int $reminderId):void { + $query = $this->db->getQueryBuilder(); + + $query->delete('calendar_reminders') + ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) + ->execute(); + } + + /** + * Cleans reminders in database + * + * @param int $objectId + */ + public function cleanRemindersForEvent(int $objectId):void { + $query = $this->db->getQueryBuilder(); + + $query->delete('calendar_reminders') + ->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId))) + ->execute(); + } + + /** + * Remove all reminders for a calendar + * + * @param int $calendarId + * @return void + */ + public function cleanRemindersForCalendar(int $calendarId):void { + $query = $this->db->getQueryBuilder(); + + $query->delete('calendar_reminders') + ->where($query->expr()->eq('calendar_id', $query->createNamedParameter($calendarId))) + ->execute(); + } + + /** + * @param array $row + * @return array + */ + private function fixRowTyping(array $row): array { + $row['id'] = (int) $row['id']; + $row['calendar_id'] = (int) $row['calendar_id']; + $row['object_id'] = (int) $row['object_id']; + $row['is_recurring'] = (bool) $row['is_recurring']; + $row['recurrence_id'] = (int) $row['recurrence_id']; + $row['is_recurrence_exception'] = (bool) $row['is_recurrence_exception']; + $row['is_relative'] = (bool) $row['is_relative']; + $row['notification_date'] = (int) $row['notification_date']; + $row['is_repeat_based'] = (bool) $row['is_repeat_based']; + + return $row; + } +} diff --git a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php new file mode 100644 index 0000000000..d0e526eb0e --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php @@ -0,0 +1,47 @@ + + * + * @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 . + * + */ +namespace OCA\DAV\CalDAV\Reminder; + +use OCP\IUser; +use Sabre\VObject\Component\VEvent; + +/** + * Interface INotificationProvider + * + * @package OCA\DAV\CalDAV\Reminder + */ +interface INotificationProvider { + + /** + * Send notification + * + * @param VEvent $vevent + * @param string $calendarDisplayName + * @param IUser[] $users + * @return void + */ + public function send(VEvent $vevent, + string $calendarDisplayName, + array $users=[]): void; +} \ No newline at end of file diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php new file mode 100644 index 0000000000..6e3a8eadde --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php @@ -0,0 +1,188 @@ + + * @author Georg Ehrke + * + * @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 . + * + */ +namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\CalDAV\Reminder\INotificationProvider; +use OCP\IConfig; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\L10N\IFactory as L10NFactory; +use OCP\IUser; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Property; + +/** + * Class AbstractProvider + * + * @package OCA\DAV\CalDAV\Reminder\NotificationProvider + */ +abstract class AbstractProvider implements INotificationProvider { + + /** @var string */ + public const NOTIFICATION_TYPE = ''; + + /** @var ILogger */ + protected $logger; + + /** @var L10NFactory */ + private $l10nFactory; + + /** @var IL10N[] */ + private $l10ns; + + /** @var string */ + private $fallbackLanguage; + + /** @var IURLGenerator */ + protected $urlGenerator; + + /** @var IConfig */ + protected $config; + + /** + * @param ILogger $logger + * @param L10NFactory $l10nFactory + * @param IConfig $config + * @param IUrlGenerator $urlGenerator + */ + public function __construct(ILogger $logger, + L10NFactory $l10nFactory, + IURLGenerator $urlGenerator, + IConfig $config) { + $this->logger = $logger; + $this->l10nFactory = $l10nFactory; + $this->urlGenerator = $urlGenerator; + $this->config = $config; + } + + /** + * Send notification + * + * @param VEvent $vevent + * @param string $calendarDisplayName + * @param IUser[] $users + * @return void + */ + abstract public function send(VEvent $vevent, + string $calendarDisplayName, + array $users=[]): void; + + /** + * @return string + */ + protected function getFallbackLanguage():string { + if ($this->fallbackLanguage) { + return $this->fallbackLanguage; + } + + $fallbackLanguage = $this->l10nFactory->findLanguage(); + $this->fallbackLanguage = $fallbackLanguage; + + return $fallbackLanguage; + } + + /** + * @param string $lang + * @return bool + */ + protected function hasL10NForLang(string $lang):bool { + return $this->l10nFactory->languageExists('dav', $lang); + } + + /** + * @param string $lang + * @return IL10N + */ + protected function getL10NForLang(string $lang):IL10N { + if (isset($this->l10ns[$lang])) { + return $this->l10ns[$lang]; + } + + $l10n = $this->l10nFactory->get('dav', $lang); + $this->l10ns[$lang] = $l10n; + + return $l10n; + } + + /** + * @param VEvent $vevent + * @return string + */ + private function getStatusOfEvent(VEvent $vevent):string { + if ($vevent->STATUS) { + return (string) $vevent->STATUS; + } + + // Doesn't say so in the standard, + // but we consider events without a status + // to be confirmed + return 'CONFIRMED'; + } + + /** + * @param VEvent $vevent + * @return bool + */ + protected function isEventTentative(VEvent $vevent):bool { + return $this->getStatusOfEvent($vevent) === 'TENTATIVE'; + } + + /** + * @param VEvent $vevent + * @return Property\ICalendar\DateTime + */ + protected function getDTEndFromEvent(VEvent $vevent):Property\ICalendar\DateTime { + if (isset($vevent->DTEND)) { + return $vevent->DTEND; + } + + if (isset($vevent->DURATION)) { + $isFloating = $vevent->DTSTART->isFloating(); + /** @var Property\ICalendar\DateTime $end */ + $end = clone $vevent->DTSTART; + $endDateTime = $end->getDateTime(); + $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue())); + $end->setDateTime($endDateTime, $isFloating); + + return $end; + } + + if (!$vevent->DTSTART->hasTime()) { + $isFloating = $vevent->DTSTART->isFloating(); + /** @var Property\ICalendar\DateTime $end */ + $end = clone $vevent->DTSTART; + $endDateTime = $end->getDateTime(); + $endDateTime = $endDateTime->modify('+1 day'); + $end->setDateTime($endDateTime, $isFloating); + + return $end; + } + + return clone $vevent->DTSTART; + } +} diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php new file mode 100644 index 0000000000..ad4ac342f6 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php @@ -0,0 +1,37 @@ + + * + * @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 + * + */ +namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; + +/** + * Class AudioProvider + * + * This class only extends PushProvider at the moment. It does not provide true + * audio-alarms yet, but it's better than no alarm at all right now. + * + * @package OCA\DAV\CalDAV\Reminder\NotificationProvider + */ +class AudioProvider extends PushProvider { + + /** @var string */ + public const NOTIFICATION_TYPE = 'AUDIO'; +} diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php new file mode 100644 index 0000000000..f5932a87b3 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php @@ -0,0 +1,503 @@ + + * @author Georg Ehrke + * + * @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 . + * + */ +namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; + +use \DateTime; +use OCP\IConfig; +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\IUser; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject; +use Sabre\VObject\Parameter; +use Sabre\VObject\Property; + +/** + * Class EmailProvider + * + * @package OCA\DAV\CalDAV\Reminder\NotificationProvider + */ +class EmailProvider extends AbstractProvider { + + /** @var string */ + public const NOTIFICATION_TYPE = 'EMAIL'; + + /** @var IMailer */ + private $mailer; + + /** + * @param IConfig $config + * @param IMailer $mailer + * @param ILogger $logger + * @param L10NFactory $l10nFactory + * @param IUrlGenerator $urlGenerator + */ + public function __construct(IConfig $config, + IMailer $mailer, + ILogger $logger, + L10NFactory $l10nFactory, + IURLGenerator $urlGenerator) { + parent::__construct($logger, $l10nFactory, $urlGenerator, $config); + $this->mailer = $mailer; + } + + /** + * Send out notification via email + * + * @param VEvent $vevent + * @param string $calendarDisplayName + * @param array $users + * @throws \Exception + */ + public function send(VEvent $vevent, + string $calendarDisplayName, + array $users=[]):void { + $fallbackLanguage = $this->getFallbackLanguage(); + + $emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users); + $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent); + + // Quote from php.net: + // If the input arrays have the same string keys, then the later value for that key will overwrite the previous one. + // => if there are duplicate email addresses, it will always take the system value + $emailAddresses = array_merge( + $emailAddressesOfAttendees, + $emailAddressesOfSharees + ); + + $sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage); + $organizer = $this->getOrganizerEMailAndNameFromEvent($vevent); + + foreach($sortedByLanguage as $lang => $emailAddresses) { + if (!$this->hasL10NForLang($lang)) { + $lang = $fallbackLanguage; + } + $l10n = $this->getL10NForLang($lang); + $fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply'); + + $template = $this->mailer->createEMailTemplate('dav.calendarReminder'); + $template->addHeader(); + $this->addSubjectAndHeading($template, $l10n, $vevent); + $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent); + $template->addFooter(); + + foreach ($emailAddresses as $emailAddress) { + $message = $this->mailer->createMessage(); + $message->setFrom([$fromEMail]); + if ($organizer) { + $message->setReplyTo($organizer); + } + $message->setTo([$emailAddress]); + $message->useTemplate($template); + + try { + $failed = $this->mailer->send($message); + if ($failed) { + $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]); + } + } catch (\Exception $ex) { + $this->logger->logException($ex, ['app' => 'dav']); + } + } + } + } + + /** + * @param IEMailTemplate $template + * @param IL10N $l10n + * @param VEvent $vevent + */ + private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void { + $template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n)); + $template->addHeading($this->getTitleFromVEvent($vevent, $l10n)); + } + + /** + * @param IEMailTemplate $template + * @param IL10N $l10n + * @param string $calendarDisplayName + * @param array $eventData + */ + private function addBulletList(IEMailTemplate $template, + IL10N $l10n, + string $calendarDisplayName, + VEvent $vevent):void { + $template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'), + $this->getAbsoluteImagePath('actions/info.svg')); + + $template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'), + $this->getAbsoluteImagePath('places/calendar.svg')); + + if (isset($vevent->LOCATION)) { + $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'), + $this->getAbsoluteImagePath('actions/address.svg')); + } + if (isset($vevent->DESCRIPTION)) { + $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'), + $this->getAbsoluteImagePath('actions/more.svg')); + } + } + + /** + * @param string $path + * @return string + */ + private function getAbsoluteImagePath(string $path):string { + return $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath('core', $path) + ); + } + + /** + * @param VEvent $vevent + * @return array|null + */ + private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array { + if (!$vevent->ORGANIZER) { + return null; + } + + $organizer = $vevent->ORGANIZER; + if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) { + return null; + } + + $organizerEMail = substr($organizer->getValue(), 7); + + $name = $organizer->offsetGet('CN'); + if ($name instanceof Parameter) { + return [$organizerEMail => $name]; + } + + return [$organizerEMail]; + } + + /** + * @param array $emails + * @param string $defaultLanguage + * @return array + */ + private function sortEMailAddressesByLanguage(array $emails, + string $defaultLanguage):array { + $sortedByLanguage = []; + + foreach($emails as $emailAddress => $parameters) { + if (isset($parameters['LANG'])) { + $lang = $parameters['LANG']; + } else { + $lang = $defaultLanguage; + } + + if (!isset($sortedByLanguage[$lang])) { + $sortedByLanguage[$lang] = []; + } + + $sortedByLanguage[$lang][] = $emailAddress; + } + + return $sortedByLanguage; + } + + /** + * @param VEvent $vevent + * @return array + */ + private function getAllEMailAddressesFromEvent(VEvent $vevent):array { + $emailAddresses = []; + + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if (!($attendee instanceof VObject\Property)) { + continue; + } + + $cuType = $this->getCUTypeOfAttendee($attendee); + if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) { + // Don't send emails to things + continue; + } + + $partstat = $this->getPartstatOfAttendee($attendee); + if ($partstat === 'DECLINED') { + // Don't send out emails to people who declined + continue; + } + if ($partstat === 'DELEGATED') { + $delegates = $attendee->offsetGet('DELEGATED-TO'); + if (!($delegates instanceof VObject\Parameter)) { + continue; + } + + $emailAddressesOfDelegates = $delegates->getParts(); + foreach($emailAddressesOfDelegates as $addressesOfDelegate) { + if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) { + $emailAddresses[substr($addressesOfDelegate, 7)] = []; + } + } + + continue; + } + + $emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee); + if ($emailAddressOfAttendee !== null) { + $properties = []; + + $langProp = $attendee->offsetGet('LANG'); + if ($langProp instanceof VObject\Parameter) { + $properties['LANG'] = $langProp->getValue(); + } + + $emailAddresses[$emailAddressOfAttendee] = $properties; + } + } + } + + if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) { + $emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = []; + } + + return $emailAddresses; + } + + + + /** + * @param VObject\Property $attendee + * @return string + */ + private function getCUTypeOfAttendee(VObject\Property $attendee):string { + $cuType = $attendee->offsetGet('CUTYPE'); + if ($cuType instanceof VObject\Parameter) { + return strtoupper($cuType->getValue()); + } + + return 'INDIVIDUAL'; + } + + /** + * @param VObject\Property $attendee + * @return string + */ + private function getPartstatOfAttendee(VObject\Property $attendee):string { + $partstat = $attendee->offsetGet('PARTSTAT'); + if ($partstat instanceof VObject\Parameter) { + return strtoupper($partstat->getValue()); + } + + return 'NEEDS-ACTION'; + } + + /** + * @param VObject\Property $attendee + * @return bool + */ + private function hasAttendeeMailURI(VObject\Property $attendee):bool { + return stripos($attendee->getValue(), 'mailto:') === 0; + } + + /** + * @param VObject\Property $attendee + * @return string|null + */ + private function getEMailAddressOfAttendee(VObject\Property $attendee):?string { + if (!$this->hasAttendeeMailURI($attendee)) { + return null; + } + + return substr($attendee->getValue(), 7); + } + + /** + * @param array $users + * @return array + */ + private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array { + $emailAddresses = []; + + foreach($users as $user) { + $emailAddress = $user->getEMailAddress(); + if ($emailAddress) { + $lang = $this->getLangForUser($user); + if ($lang) { + $emailAddresses[$emailAddress] = [ + 'LANG' => $lang, + ]; + } else { + $emailAddresses[$emailAddress] = []; + } + } + } + + return $emailAddresses; + } + + /** + * @param IUser $user + * @return string + */ + private function getLangForUser(IUser $user): ?string { + return $this->config->getUserValue($user->getUID(), 'core', 'lang', null); + } + + /** + * @param IL10N $l10n + * @param VEvent $vevent + * @return string + * @throws \Exception + */ + private function generateDateString(IL10N $l10n, VEvent $vevent):string { + $isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date; + + /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */ + /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */ + /** @var \DateTimeImmutable $dtstartDt */ + $dtstartDt = $vevent->DTSTART->getDateTime(); + /** @var \DateTimeImmutable $dtendDt */ + $dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime(); + + $diff = $dtstartDt->diff($dtendDt); + + /** @phan-suppress-next-line PhanUndeclaredClassMethod */ + $dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM)); + /** @phan-suppress-next-line PhanUndeclaredClassMethod */ + $dtendDt = new \DateTime($dtendDt->format(\DateTime::ATOM)); + + if ($isAllDay) { + // One day event + if ($diff->days === 1) { + return $this->getDateString($l10n, $dtstartDt); + } + + return implode(' - ', [ + $this->getDateString($l10n, $dtstartDt), + $this->getDateString($l10n, $dtendDt), + ]); + } + + $startTimezone = $endTimezone = null; + if (!$vevent->DTSTART->isFloating()) { + /** @phan-suppress-next-line PhanUndeclaredClassMethod */ + $startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName(); + /** @phan-suppress-next-line PhanUndeclaredClassMethod */ + $endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName(); + } + + $localeStart = implode(', ', [ + $this->getWeekDayName($l10n, $dtstartDt), + $this->getDateTimeString($l10n, $dtstartDt) + ]); + + // always show full date with timezone if timezones are different + if ($startTimezone !== $endTimezone) { + $localeEnd = implode(', ', [ + $this->getWeekDayName($l10n, $dtendDt), + $this->getDateTimeString($l10n, $dtendDt) + ]); + + return $localeStart + . ' (' . $startTimezone . ') ' + . ' - ' + . $localeEnd + . ' (' . $endTimezone . ')'; + } + + // Show only the time if the day is the same + $localeEnd = $this->isDayEqual($dtstartDt, $dtendDt) + ? $this->getTimeString($l10n, $dtendDt) + : implode(', ', [ + $this->getWeekDayName($l10n, $dtendDt), + $this->getDateTimeString($l10n, $dtendDt) + ]); + + return $localeStart + . ' - ' + . $localeEnd + . ' (' . $startTimezone . ')'; + } + + /** + * @param DateTime $dtStart + * @param DateTime $dtEnd + * @return bool + */ + private function isDayEqual(DateTime $dtStart, + DateTime $dtEnd):bool { + return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d'); + } + + /** + * @param IL10N $l10n + * @param DateTime $dt + * @return string + */ + private function getWeekDayName(IL10N $l10n, DateTime $dt):string { + return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']); + } + + /** + * @param IL10N $l10n + * @param DateTime $dt + * @return string + */ + private function getDateString(IL10N $l10n, DateTime $dt):string { + return $l10n->l('date', $dt, ['width' => 'medium']); + } + + /** + * @param IL10N $l10n + * @param DateTime $dt + * @return string + */ + private function getDateTimeString(IL10N $l10n, DateTime $dt):string { + return $l10n->l('datetime', $dt, ['width' => 'medium|short']); + } + + /** + * @param IL10N $l10n + * @param DateTime $dt + * @return string + */ + private function getTimeString(IL10N $l10n, DateTime $dt):string { + return $l10n->l('time', $dt, ['width' => 'short']); + } + + /** + * @param VEvent $vevent + * @param IL10N $l10n + * @return string + */ + private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string { + if (isset($vevent->SUMMARY)) { + return (string)$vevent->SUMMARY; + } + + return $l10n->t('Untitled event'); + } +} diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php new file mode 100644 index 0000000000..bfa6db9585 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php @@ -0,0 +1,39 @@ + + * + * @author Thomas Citharel + * + * @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 . + * + */ +namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; + +class ProviderNotAvailableException extends \Exception { + + /** + * ProviderNotAvailableException constructor. + * + * @since 16.0.0 + * + * @param string $type ReminderType + */ + public function __construct(string $type) { + parent::__construct("No notification provider for type $type available"); + } + +} diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php new file mode 100644 index 0000000000..3872b67e59 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php @@ -0,0 +1,139 @@ + + * @author Georg Ehrke + * + * @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 . + * + */ +namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\AppInfo\Application; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\L10N\IFactory as L10NFactory; +use OCP\Notification\IManager; +use OCP\IUser; +use OCP\Notification\INotification; +use OCP\AppFramework\Utility\ITimeFactory; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Property; + +/** + * Class PushProvider + * + * @package OCA\DAV\CalDAV\Reminder\NotificationProvider + */ +class PushProvider extends AbstractProvider { + + /** @var string */ + public const NOTIFICATION_TYPE = 'DISPLAY'; + + /** @var IManager */ + private $manager; + + /** @var ITimeFactory */ + private $timeFactory; + + /** + * @param IConfig $config + * @param IManager $manager + * @param ILogger $logger + * @param L10NFactory $l10nFactory + * @param IUrlGenerator $urlGenerator + * @param ITimeFactory $timeFactory + */ + public function __construct(IConfig $config, + IManager $manager, + ILogger $logger, + L10NFactory $l10nFactory, + IURLGenerator $urlGenerator, + ITimeFactory $timeFactory) { + parent::__construct($logger, $l10nFactory, $urlGenerator, $config); + $this->manager = $manager; + $this->timeFactory = $timeFactory; + } + + /** + * Send push notification to all users. + * + * @param VEvent $vevent + * @param string $calendarDisplayName + * @param IUser[] $users + * @throws \Exception + */ + public function send(VEvent $vevent, + string $calendarDisplayName=null, + array $users=[]):void { + $eventDetails = $this->extractEventDetails($vevent); + $eventDetails['calendar_displayname'] = $calendarDisplayName; + + foreach($users as $user) { + /** @var INotification $notification */ + $notification = $this->manager->createNotification(); + $notification->setApp(Application::APP_ID) + ->setUser($user->getUID()) + ->setDateTime($this->timeFactory->getDateTime()) + ->setObject(Application::APP_ID, (string) $vevent->UID) + ->setSubject('calendar_reminder', [ + 'title' => $eventDetails['title'], + 'start_atom' => $eventDetails['start_atom'] + ]) + ->setMessage('calendar_reminder', $eventDetails); + + $this->manager->notify($notification); + } + } + + /** + * @var VEvent $vevent + * @return array + * @throws \Exception + */ + protected function extractEventDetails(VEvent $vevent):array { + /** @var Property\ICalendar\DateTime $start */ + $start = $vevent->DTSTART; + $end = $this->getDTEndFromEvent($vevent); + + return [ + 'title' => isset($vevent->SUMMARY) + ? ((string) $vevent->SUMMARY) + : null, + 'description' => isset($vevent->DESCRIPTION) + ? ((string) $vevent->DESCRIPTION) + : null, + 'location' => isset($vevent->LOCATION) + ? ((string) $vevent->LOCATION) + : null, + 'all_day' => $start instanceof Property\ICalendar\Date, + /** @phan-suppress-next-line PhanUndeclaredClassMethod */ + 'start_atom' => $start->getDateTime()->format(\DateTime::ATOM), + 'start_is_floating' => $start->isFloating(), + /** @phan-suppress-next-line PhanUndeclaredClassMethod */ + 'start_timezone' => $start->getDateTime()->getTimezone()->getName(), + /** @phan-suppress-next-line PhanUndeclaredClassMethod */ + 'end_atom' => $end->getDateTime()->format(\DateTime::ATOM), + 'end_is_floating' => $end->isFloating(), + /** @phan-suppress-next-line PhanUndeclaredClassMethod */ + 'end_timezone' => $end->getDateTime()->getTimezone()->getName(), + ]; + } +} diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php new file mode 100644 index 0000000000..c9bcf2dd06 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php @@ -0,0 +1,81 @@ + + * @author Georg Ehrke + * + * @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 + * + */ +namespace OCA\DAV\CalDAV\Reminder; + +/** + * Class NotificationProviderManager + * + * @package OCA\DAV\CalDAV\Reminder + */ +class NotificationProviderManager { + + /** @var INotificationProvider[] */ + private $providers = []; + + /** + * Checks whether a provider for a given ACTION exists + * + * @param string $type + * @return bool + */ + public function hasProvider(string $type):bool { + return (\in_array($type, ReminderService::REMINDER_TYPES, true) + && isset($this->providers[$type])); + } + + /** + * Get provider for a given ACTION + * + * @param string $type + * @return INotificationProvider + * @throws NotificationProvider\ProviderNotAvailableException + * @throws NotificationTypeDoesNotExistException + */ + public function getProvider(string $type):INotificationProvider { + if (in_array($type, ReminderService::REMINDER_TYPES, true)) { + if (isset($this->providers[$type])) { + return $this->providers[$type]; + } + throw new NotificationProvider\ProviderNotAvailableException($type); + } + throw new NotificationTypeDoesNotExistException($type); + } + + /** + * Registers a new provider + * + * @param string $providerClassName + * @throws \OCP\AppFramework\QueryException + */ + public function registerProvider(string $providerClassName):void { + $provider = \OC::$server->query($providerClassName); + + if (!$provider instanceof INotificationProvider) { + throw new \InvalidArgumentException('Invalid notification provider registered'); + } + + $this->providers[$provider::NOTIFICATION_TYPE] = $provider; + } +} diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php new file mode 100644 index 0000000000..c060089785 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php @@ -0,0 +1,40 @@ + + * + * @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 . + * + */ + +namespace OCA\DAV\CalDAV\Reminder; + +class NotificationTypeDoesNotExistException extends \Exception { + + /** + * NotificationTypeDoesNotExistException constructor. + * + * @since 16.0.0 + * + * @param string $type ReminderType + */ + public function __construct(string $type) { + parent::__construct("Type $type is not an accepted type of notification"); + } + +} diff --git a/apps/dav/lib/CalDAV/Reminder/Notifier.php b/apps/dav/lib/CalDAV/Reminder/Notifier.php new file mode 100644 index 0000000000..2c5f05f62a --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/Notifier.php @@ -0,0 +1,316 @@ + + * @author Georg Ehrke + * + * @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 + * + */ + +namespace OCA\DAV\CalDAV\Reminder; + +use \DateTime; +use OCA\DAV\AppInfo\Application; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IL10N; +use OCP\L10N\IFactory; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; +use OCP\IURLGenerator; + +/** + * Class Notifier + * + * @package OCA\DAV\CalDAV\Reminder + */ +class Notifier implements INotifier { + + /** @var IFactory */ + private $l10nFactory; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** @var IL10N */ + private $l10n; + + /** @var ITimeFactory */ + private $timeFactory; + + /** + * Notifier constructor. + * + * @param IFactory $factory + * @param IURLGenerator $urlGenerator + * @param ITimeFactory $timeFactory + */ + public function __construct(IFactory $factory, + IURLGenerator $urlGenerator, + ITimeFactory $timeFactory) { + $this->l10nFactory = $factory; + $this->urlGenerator = $urlGenerator; + $this->timeFactory = $timeFactory; + } + + /** + * Identifier of the notifier, only use [a-z0-9_] + * + * @return string + * @since 17.0.0 + */ + public function getID():string { + return Application::APP_ID; + } + + /** + * Human readable name describing the notifier + * + * @return string + * @since 17.0.0 + */ + public function getName():string { + return $this->l10nFactory->get('dav')->t('Calendar'); + } + + /** + * Prepare sending the notification + * + * @param INotification $notification + * @param string $languageCode The code of the language that should be used to prepare the notification + * @return INotification + * @throws \Exception + */ + public function prepare(INotification $notification, + string $languageCode):INotification { + if ($notification->getApp() !== Application::APP_ID) { + throw new \InvalidArgumentException('Notification not from this app'); + } + + // Read the language from the notification + $this->l10n = $this->l10nFactory->get('dav', $languageCode); + + // Handle notifier subjects + switch($notification->getSubject()) { + case 'calendar_reminder': + return $this->prepareReminderNotification($notification); + + default: + throw new \InvalidArgumentException('Unknown subject'); + + } + } + + /** + * @param INotification $notification + * @return INotification + */ + private function prepareReminderNotification(INotification $notification):INotification { + $imagePath = $this->urlGenerator->imagePath('core', 'places/calendar.svg'); + $iconUrl = $this->urlGenerator->getAbsoluteURL($imagePath); + $notification->setIcon($iconUrl); + + $this->prepareNotificationSubject($notification); + $this->prepareNotificationMessage($notification); + + return $notification; + } + + /** + * Sets the notification subject based on the parameters set in PushProvider + * + * @param INotification $notification + */ + private function prepareNotificationSubject(INotification $notification): void { + $parameters = $notification->getSubjectParameters(); + + $startTime = \DateTime::createFromFormat(\DateTime::ATOM, $parameters['start_atom']); + $now = $this->timeFactory->getDateTime(); + $title = $this->getTitleFromParameters($parameters); + + $diff = $startTime->diff($now); + if ($diff === false) { + return; + } + + $components = []; + if ($diff->y) { + $components[] = $this->l10n->n('%n year', '%n years', $diff->y); + } + if ($diff->m) { + $components[] = $this->l10n->n('%n month', '%n months', $diff->m); + } + if ($diff->d) { + $components[] = $this->l10n->n('%n day', '%n days', $diff->d); + } + if ($diff->h) { + $components[] = $this->l10n->n('%n hour', '%n hours', $diff->h); + } + if ($diff->i) { + $components[] = $this->l10n->n('%n minute', '%n minutes', $diff->i); + } + + // Limiting to the first three components to prevent + // the string from getting too long + $firstThreeComponents = array_slice($components, 0, 2); + $diffLabel = implode(', ', $firstThreeComponents); + + if ($diff->invert) { + $title = $this->l10n->t('%s (in %s)', [$title, $diffLabel]); + } else { + $title = $this->l10n->t('%s (%s ago)', [$title, $diffLabel]); + } + + $notification->setParsedSubject($title); + } + + /** + * Sets the notification message based on the parameters set in PushProvider + * + * @param INotification $notification + */ + private function prepareNotificationMessage(INotification $notification): void { + $parameters = $notification->getMessageParameters(); + + $description = [ + $this->l10n->t('Calendar: %s', $parameters['calendar_displayname']), + $this->l10n->t('Date: %s', $this->generateDateString($parameters)), + ]; + if ($parameters['description']) { + $description[] = $this->l10n->t('Description: %s', $parameters['description']); + } + if ($parameters['location']) { + $description[] = $this->l10n->t('Where: %s', $parameters['location']); + } + + $message = implode("\r\n", $description); + $notification->setParsedMessage($message); + } + + /** + * @param array $parameters + * @return string + */ + private function getTitleFromParameters(array $parameters):string { + return $parameters['title'] ?? $this->l10n->t('Untitled event'); + } + + /** + * @param array $parameters + * @return string + * @throws \Exception + */ + private function generateDateString(array $parameters):string { + $startDateTime = DateTime::createFromFormat(\DateTime::ATOM, $parameters['start_atom']); + $endDateTime = DateTime::createFromFormat(\DateTime::ATOM, $parameters['end_atom']); + $isAllDay = $parameters['all_day']; + $diff = $startDateTime->diff($endDateTime); + + if ($isAllDay) { + // One day event + if ($diff->days === 1) { + return $this->getDateString($startDateTime); + } + + return implode(' - ', [ + $this->getDateString($startDateTime), + $this->getDateString($endDateTime), + ]); + } + + $startTimezone = $endTimezone = null; + if (!$parameters['start_is_floating']) { + $startTimezone = $parameters['start_timezone']; + $endTimezone = $parameters['end_timezone']; + } + + $localeStart = implode(', ', [ + $this->getWeekDayName($startDateTime), + $this->getDateTimeString($startDateTime) + ]); + + // always show full date with timezone if timezones are different + if ($startTimezone !== $endTimezone) { + $localeEnd = implode(', ', [ + $this->getWeekDayName($endDateTime), + $this->getDateTimeString($endDateTime) + ]); + + return $localeStart + . ' (' . $startTimezone . ') ' + . ' - ' + . $localeEnd + . ' (' . $endTimezone . ')'; + } + + // Show only the time if the day is the same + $localeEnd = $this->isDayEqual($startDateTime, $endDateTime) + ? $this->getTimeString($endDateTime) + : implode(', ', [ + $this->getWeekDayName($endDateTime), + $this->getDateTimeString($endDateTime) + ]); + + return $localeStart + . ' - ' + . $localeEnd + . ' (' . $startTimezone . ')'; + } + + /** + * @param DateTime $dtStart + * @param DateTime $dtEnd + * @return bool + */ + private function isDayEqual(DateTime $dtStart, + DateTime $dtEnd):bool { + return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d'); + } + + /** + * @param DateTime $dt + * @return string + */ + private function getWeekDayName(DateTime $dt):string { + return $this->l10n->l('weekdayName', $dt, ['width' => 'abbreviated']); + } + + /** + * @param DateTime $dt + * @return string + */ + private function getDateString(DateTime $dt):string { + return $this->l10n->l('date', $dt, ['width' => 'medium']); + } + + /** + * @param DateTime $dt + * @return string + */ + private function getDateTimeString(DateTime $dt):string { + return $this->l10n->l('datetime', $dt, ['width' => 'medium|short']); + } + + /** + * @param DateTime $dt + * @return string + */ + private function getTimeString(DateTime $dt):string { + return $this->l10n->l('time', $dt, ['width' => 'short']); + } +} diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php new file mode 100644 index 0000000000..f36ddd157c --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php @@ -0,0 +1,761 @@ + + * @author Georg Ehrke + * + * @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 + * + */ +namespace OCA\DAV\CalDAV\Reminder; + +use \DateTimeImmutable; +use OCA\DAV\CalDAV\CalDavBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use Sabre\VObject; +use Sabre\VObject\Component\VAlarm; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\ParseException; +use Sabre\VObject\Recur\EventIterator; +use Sabre\VObject\Recur\NoInstancesException; + +class ReminderService { + + /** @var Backend */ + private $backend; + + /** @var NotificationProviderManager */ + private $notificationProviderManager; + + /** @var IUserManager */ + private $userManager; + + /** @var IGroupManager */ + private $groupManager; + + /** @var CalDavBackend */ + private $caldavBackend; + + /** @var ITimeFactory */ + private $timeFactory; + + public const REMINDER_TYPE_EMAIL = 'EMAIL'; + public const REMINDER_TYPE_DISPLAY = 'DISPLAY'; + public const REMINDER_TYPE_AUDIO = 'AUDIO'; + + /** + * @var String[] + * + * Official RFC5545 reminder types + */ + public const REMINDER_TYPES = [ + self::REMINDER_TYPE_EMAIL, + self::REMINDER_TYPE_DISPLAY, + self::REMINDER_TYPE_AUDIO + ]; + + /** + * ReminderService constructor. + * + * @param Backend $backend + * @param NotificationProviderManager $notificationProviderManager + * @param IUserManager $userManager + * @param IGroupManager $groupManager + * @param CalDavBackend $caldavBackend + * @param ITimeFactory $timeFactory + */ + public function __construct(Backend $backend, + NotificationProviderManager $notificationProviderManager, + IUserManager $userManager, + IGroupManager $groupManager, + CalDavBackend $caldavBackend, + ITimeFactory $timeFactory) { + $this->backend = $backend; + $this->notificationProviderManager = $notificationProviderManager; + $this->userManager = $userManager; + $this->groupManager = $groupManager; + $this->caldavBackend = $caldavBackend; + $this->timeFactory = $timeFactory; + } + + /** + * Process reminders to activate + * + * @throws NotificationProvider\ProviderNotAvailableException + * @throws NotificationTypeDoesNotExistException + */ + public function processReminders():void { + $reminders = $this->backend->getRemindersToProcess(); + + foreach($reminders as $reminder) { + $vcalendar = $this->parseCalendarData($reminder['calendardata']); + if (!$vcalendar) { + $this->backend->removeReminder($reminder['id']); + continue; + } + + $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']); + if (!$vevent) { + $this->backend->removeReminder($reminder['id']); + continue; + } + + if ($this->wasEventCancelled($vevent)) { + $this->deleteOrProcessNext($reminder, $vevent); + continue; + } + + if (!$this->notificationProviderManager->hasProvider($reminder['type'])) { + $this->deleteOrProcessNext($reminder, $vevent); + continue; + } + + $users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']); + $user = $this->getUserFromPrincipalURI($reminder['principaluri']); + if ($user) { + $users[] = $user; + } + + $notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']); + $notificationProvider->send($vevent, $reminder['displayname'], $users); + + $this->deleteOrProcessNext($reminder, $vevent); + } + } + + /** + * @param string $action + * @param array $objectData + * @throws VObject\InvalidDataException + */ + public function onTouchCalendarObject(string $action, + array $objectData):void { + // We only support VEvents for now + if (strcasecmp($objectData['component'], 'vevent') !== 0) { + return; + } + + switch($action) { + case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject': + $this->onCalendarObjectCreate($objectData); + break; + + case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject': + $this->onCalendarObjectEdit($objectData); + break; + + case '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject': + $this->onCalendarObjectDelete($objectData); + break; + + default: + break; + } + } + + /** + * @param array $objectData + */ + private function onCalendarObjectCreate(array $objectData):void { + /** @var VObject\Component\VCalendar $vcalendar */ + $vcalendar = $this->parseCalendarData($objectData['calendardata']); + if (!$vcalendar) { + return; + } + + $vevents = $this->getAllVEventsFromVCalendar($vcalendar); + if (count($vevents) === 0) { + return; + } + + $uid = (string) $vevents[0]->UID; + $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); + $masterItem = $this->getMasterItemFromListOfVEvents($vevents); + $now = $this->timeFactory->getDateTime(); + $isRecurring = $masterItem ? $this->isRecurring($masterItem) : false; + + foreach($recurrenceExceptions as $recurrenceException) { + $eventHash = $this->getEventHash($recurrenceException); + + foreach($recurrenceException->VALARM as $valarm) { + /** @var VAlarm $valarm */ + $alarmHash = $this->getAlarmHash($valarm); + $triggerTime = $valarm->getEffectiveTriggerTime(); + $diff = $now->diff($triggerTime); + if ($diff->invert === 1) { + continue; + } + + $alarms = $this->getRemindersForVAlarm($valarm, $objectData, + $eventHash, $alarmHash, true, true); + $this->writeRemindersToDatabase($alarms); + } + } + + if ($masterItem) { + $processedAlarms = []; + $masterAlarms = []; + $masterHash = $this->getEventHash($masterItem); + + foreach($masterItem->VALARM as $valarm) { + $masterAlarms[] = $this->getAlarmHash($valarm); + } + + try { + $iterator = new EventIterator($vevents, $uid); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + return; + } + + while($iterator->valid() && count($processedAlarms) < count($masterAlarms)) { + $event = $iterator->getEventObject(); + + // Recurrence-exceptions are handled separately, so just ignore them here + if (\in_array($event, $recurrenceExceptions, true)) { + $iterator->next(); + continue; + } + + foreach($event->VALARM as $valarm) { + /** @var VAlarm $valarm */ + $alarmHash = $this->getAlarmHash($valarm); + if (\in_array($alarmHash, $processedAlarms, true)) { + continue; + } + + if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) { + // Action allows x-name, we don't insert reminders + // into the database if they are not standard + $processedAlarms[] = $alarmHash; + continue; + } + + $triggerTime = $valarm->getEffectiveTriggerTime(); + + // If effective trigger time is in the past + // just skip and generate for next event + $diff = $now->diff($triggerTime); + if ($diff->invert === 1) { + // If an absolute alarm is in the past, + // just add it to processedAlarms, so + // we don't extend till eternity + if (!$this->isAlarmRelative($valarm)) { + $processedAlarms[] = $alarmHash; + } + + continue; + } + + $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false); + $this->writeRemindersToDatabase($alarms); + $processedAlarms[] = $alarmHash; + } + + $iterator->next(); + } + } + } + + /** + * @param array $objectData + */ + private function onCalendarObjectEdit(array $objectData):void { + // TODO - this can be vastly improved + // - get cached reminders + // - ... + + $this->onCalendarObjectDelete($objectData); + $this->onCalendarObjectCreate($objectData); + } + + /** + * @param array $objectData + */ + private function onCalendarObjectDelete(array $objectData):void { + $this->backend->cleanRemindersForEvent((int) $objectData['id']); + } + + /** + * @param VAlarm $valarm + * @param array $objectData + * @param string|null $eventHash + * @param string|null $alarmHash + * @param bool $isRecurring + * @param bool $isRecurrenceException + * @return array + */ + private function getRemindersForVAlarm(VAlarm $valarm, + array $objectData, + string $eventHash=null, + string $alarmHash=null, + bool $isRecurring=false, + bool $isRecurrenceException=false):array { + if ($eventHash === null) { + $eventHash = $this->getEventHash($valarm->parent); + } + if ($alarmHash === null) { + $alarmHash = $this->getAlarmHash($valarm); + } + + $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent); + $isRelative = $this->isAlarmRelative($valarm); + /** @var DateTimeImmutable $notificationDate */ + $notificationDate = $valarm->getEffectiveTriggerTime(); + $clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone()); + $clonedNotificationDate->setTimestamp($notificationDate->getTimestamp()); + + $alarms = []; + + $alarms[] = [ + 'calendar_id' => $objectData['calendarid'], + 'object_id' => $objectData['id'], + 'uid' => (string) $valarm->parent->UID, + 'is_recurring' => $isRecurring, + 'recurrence_id' => $recurrenceId, + 'is_recurrence_exception' => $isRecurrenceException, + 'event_hash' => $eventHash, + 'alarm_hash' => $alarmHash, + 'type' => (string) $valarm->ACTION, + 'is_relative' => $isRelative, + 'notification_date' => $notificationDate->getTimestamp(), + 'is_repeat_based' => false, + ]; + + $repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0; + for($i = 0; $i < $repeat; $i++) { + if ($valarm->DURATION === null) { + continue; + } + + $clonedNotificationDate->add($valarm->DURATION->getDateInterval()); + $alarms[] = [ + 'calendar_id' => $objectData['calendarid'], + 'object_id' => $objectData['id'], + 'uid' => (string) $valarm->parent->UID, + 'is_recurring' => $isRecurring, + 'recurrence_id' => $recurrenceId, + 'is_recurrence_exception' => $isRecurrenceException, + 'event_hash' => $eventHash, + 'alarm_hash' => $alarmHash, + 'type' => (string) $valarm->ACTION, + 'is_relative' => $isRelative, + 'notification_date' => $clonedNotificationDate->getTimestamp(), + 'is_repeat_based' => true, + ]; + } + + return $alarms; + } + + /** + * @param array $reminders + */ + private function writeRemindersToDatabase(array $reminders): void { + foreach($reminders as $reminder) { + $this->backend->insertReminder( + (int) $reminder['calendar_id'], + (int) $reminder['object_id'], + $reminder['uid'], + $reminder['is_recurring'], + (int) $reminder['recurrence_id'], + $reminder['is_recurrence_exception'], + $reminder['event_hash'], + $reminder['alarm_hash'], + $reminder['type'], + $reminder['is_relative'], + (int) $reminder['notification_date'], + $reminder['is_repeat_based'] + ); + } + } + + /** + * @param array $reminder + * @param VEvent $vevent + */ + private function deleteOrProcessNext(array $reminder, + VObject\Component\VEvent $vevent):void { + if ($reminder['is_repeat_based'] || + !$reminder['is_recurring'] || + !$reminder['is_relative'] || + $reminder['is_recurrence_exception']) { + + $this->backend->removeReminder($reminder['id']); + return; + } + + $vevents = $this->getAllVEventsFromVCalendar($vevent->parent); + $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); + $now = $this->timeFactory->getDateTime(); + + try { + $iterator = new EventIterator($vevents, $reminder['uid']); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + return; + } + + while($iterator->valid()) { + $event = $iterator->getEventObject(); + + // Recurrence-exceptions are handled separately, so just ignore them here + if (\in_array($event, $recurrenceExceptions, true)) { + $iterator->next(); + continue; + } + + $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event); + if ($reminder['recurrence_id'] >= $recurrenceId) { + $iterator->next(); + continue; + } + + foreach($event->VALARM as $valarm) { + /** @var VAlarm $valarm */ + $alarmHash = $this->getAlarmHash($valarm); + if ($alarmHash !== $reminder['alarm_hash']) { + continue; + } + + $triggerTime = $valarm->getEffectiveTriggerTime(); + + // If effective trigger time is in the past + // just skip and generate for next event + $diff = $now->diff($triggerTime); + if ($diff->invert === 1) { + continue; + } + + $this->backend->removeReminder($reminder['id']); + $alarms = $this->getRemindersForVAlarm($valarm, [ + 'calendarid' => $reminder['calendar_id'], + 'id' => $reminder['object_id'], + ], $reminder['event_hash'], $alarmHash, true, false); + $this->writeRemindersToDatabase($alarms); + + // Abort generating reminders after creating one successfully + return; + } + + $iterator->next(); + } + + $this->backend->removeReminder($reminder['id']); + } + + /** + * @param int $calendarId + * @return IUser[] + */ + private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array { + $shares = $this->caldavBackend->getShares($calendarId); + + $users = []; + $userIds = []; + $groups = []; + foreach ($shares as $share) { + // Only consider writable shares + if ($share['readOnly']) { + continue; + } + + $principal = explode('/', $share['{http://owncloud.org/ns}principal']); + if ($principal[1] === 'users') { + $user = $this->userManager->get($principal[2]); + if ($user) { + $users[] = $user; + $userIds[] = $principal[2]; + } + } else if ($principal[1] === 'groups') { + $groups[] = $principal[2]; + } + } + + foreach ($groups as $gid) { + $group = $this->groupManager->get($gid); + if ($group instanceof IGroup) { + foreach ($group->getUsers() as $user) { + if (!\in_array($user->getUID(), $userIds, true)) { + $users[] = $user; + $userIds[] = $user->getUID(); + } + } + } + } + + return $users; + } + + /** + * Gets a hash of the event. + * If the hash changes, we have to update all relative alarms. + * + * @param VEvent $vevent + * @return string + */ + private function getEventHash(VEvent $vevent):string { + $properties = [ + (string) $vevent->DTSTART->serialize(), + ]; + + if ($vevent->DTEND) { + $properties[] = (string) $vevent->DTEND->serialize(); + } + if ($vevent->DURATION) { + $properties[] = (string) $vevent->DURATION->serialize(); + } + if ($vevent->{'RECURRENCE-ID'}) { + $properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize(); + } + if ($vevent->RRULE) { + $properties[] = (string) $vevent->RRULE->serialize(); + } + if ($vevent->EXDATE) { + $properties[] = (string) $vevent->EXDATE->serialize(); + } + if ($vevent->RDATE) { + $properties[] = (string) $vevent->RDATE->serialize(); + } + + return md5(implode('::', $properties)); + } + + /** + * Gets a hash of the alarm. + * If the hash changes, we have to update oc_dav_reminders. + * + * @param VAlarm $valarm + * @return string + */ + private function getAlarmHash(VAlarm $valarm):string { + $properties = [ + (string) $valarm->ACTION->serialize(), + (string) $valarm->TRIGGER->serialize(), + ]; + + if ($valarm->DURATION) { + $properties[] = (string) $valarm->DURATION->serialize(); + } + if ($valarm->REPEAT) { + $properties[] = (string) $valarm->REPEAT->serialize(); + } + + return md5(implode('::', $properties)); + } + + /** + * @param VObject\Component\VCalendar $vcalendar + * @param int $recurrenceId + * @param bool $isRecurrenceException + * @return VEvent|null + */ + private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar, + int $recurrenceId, + bool $isRecurrenceException):?VEvent { + $vevents = $this->getAllVEventsFromVCalendar($vcalendar); + if (count($vevents) === 0) { + return null; + } + + $uid = (string) $vevents[0]->UID; + $recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents); + $masterItem = $this->getMasterItemFromListOfVEvents($vevents); + + // Handle recurrence-exceptions first, because recurrence-expansion is expensive + if ($isRecurrenceException) { + foreach($recurrenceExceptions as $recurrenceException) { + if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) { + return $recurrenceException; + } + } + + return null; + } + + if ($masterItem) { + try { + $iterator = new EventIterator($vevents, $uid); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + return null; + } + + while ($iterator->valid()) { + $event = $iterator->getEventObject(); + + // Recurrence-exceptions are handled separately, so just ignore them here + if (\in_array($event, $recurrenceExceptions, true)) { + $iterator->next(); + continue; + } + + if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) { + return $event; + } + + $iterator->next(); + } + } + + return null; + } + + /** + * @param VEvent $vevent + * @return string + */ + private function getStatusOfEvent(VEvent $vevent):string { + if ($vevent->STATUS) { + return (string) $vevent->STATUS; + } + + // Doesn't say so in the standard, + // but we consider events without a status + // to be confirmed + return 'CONFIRMED'; + } + + /** + * @param VObject\Component\VEvent $vevent + * @return bool + */ + private function wasEventCancelled(VObject\Component\VEvent $vevent):bool { + return $this->getStatusOfEvent($vevent) === 'CANCELLED'; + } + + /** + * @param string $calendarData + * @return VObject\Component\VCalendar|null + */ + private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar { + try { + return VObject\Reader::read($calendarData, + VObject\Reader::OPTION_FORGIVING); + } catch(ParseException $ex) { + return null; + } + } + + /** + * @param string $principalUri + * @return IUser|null + */ + private function getUserFromPrincipalURI(string $principalUri):?IUser { + if (!$principalUri) { + return null; + } + + if (stripos($principalUri, 'principals/users/') !== 0) { + return null; + } + + $userId = substr($principalUri, 17); + return $this->userManager->get($userId); + } + + /** + * @param VObject\Component\VCalendar $vcalendar + * @return VObject\Component\VEvent[] + */ + private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array { + $vevents = []; + + foreach($vcalendar->children() as $child) { + if (!($child instanceof VObject\Component)) { + continue; + } + + if ($child->name !== 'VEVENT') { + continue; + } + + $vevents[] = $child; + } + + return $vevents; + } + + /** + * @param array $vevents + * @return VObject\Component\VEvent[] + */ + private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array { + return array_values(array_filter($vevents, function(VEvent $vevent) { + return $vevent->{'RECURRENCE-ID'} !== null; + })); + } + + /** + * @param array $vevents + * @return VEvent|null + */ + private function getMasterItemFromListOfVEvents(array $vevents):?VEvent { + $elements = array_values(array_filter($vevents, function(VEvent $vevent) { + return $vevent->{'RECURRENCE-ID'} === null; + })); + + if (count($elements) === 0) { + return null; + } + if (count($elements) > 1) { + throw new \TypeError('Multiple master objects'); + } + + return $elements[0]; + } + + /** + * @param VAlarm $valarm + * @return bool + */ + private function isAlarmRelative(VAlarm $valarm):bool { + $trigger = $valarm->TRIGGER; + return $trigger instanceof VObject\Property\ICalendar\Duration; + } + + /** + * @param VEvent $vevent + * @return int + */ + private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int { + if (isset($vevent->{'RECURRENCE-ID'})) { + return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp(); + } + + return $vevent->DTSTART->getDateTime()->getTimestamp(); + } + + /** + * @param VEvent $vevent + * @return bool + */ + private function isRecurring(VEvent $vevent):bool { + return isset($vevent->RRULE) || isset($vevent->RDATE); + } +} diff --git a/apps/dav/lib/Command/SendEventReminders.php b/apps/dav/lib/Command/SendEventReminders.php new file mode 100644 index 0000000000..93477cb0f7 --- /dev/null +++ b/apps/dav/lib/Command/SendEventReminders.php @@ -0,0 +1,82 @@ + + * + * @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 + * + */ +namespace OCA\DAV\Command; + +use OCA\DAV\CalDAV\Reminder\ReminderService; +use OCP\IConfig; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Class SendEventReminders + * + * @package OCA\DAV\Command + */ +class SendEventReminders extends Command { + + /** @var ReminderService */ + protected $reminderService; + + /** @var IConfig */ + protected $config; + + /** + * @param ReminderService $reminderService + * @param IConfig $config + */ + public function __construct(ReminderService $reminderService, + IConfig $config) { + parent::__construct(); + $this->reminderService = $reminderService; + $this->config = $config; + } + + /** + * @inheritDoc + */ + protected function configure():void { + $this + ->setName('dav:send-event-reminders') + ->setDescription('Sends event reminders'); + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function execute(InputInterface $input, OutputInterface $output):void { + if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') { + $output->writeln('Sending event reminders disabled!'); + $output->writeln('Please run "php occ config:app:set dav sendEventReminders --value yes"'); + return; + } + + if ($this->config->getAppValue('dav', 'sendEventRemindersMode', 'backgroundjob') !== 'occ') { + $output->writeln('Sending event reminders mode set to background-job!'); + $output->writeln('Please run "php occ config:app:set dav sendEventRemindersMode --value occ"'); + return; + } + + $this->reminderService->processReminders(); + } +} diff --git a/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php b/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php new file mode 100644 index 0000000000..c7a1cf63ab --- /dev/null +++ b/apps/dav/lib/Migration/RegisterBuildReminderIndexBackgroundJob.php @@ -0,0 +1,96 @@ + + * + * @author Georg Ehrke + * + * @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 . + * + */ +namespace OCA\DAV\Migration; + +use OCA\DAV\BackgroundJob\BuildReminderIndexBackgroundJob; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +/** + * Class RegisterBuildReminderIndexBackgroundJob + * + * @package OCA\DAV\Migration + */ +class RegisterBuildReminderIndexBackgroundJob implements IRepairStep { + + /** @var IDBConnection */ + private $db; + + /** @var IJobList */ + private $jobList; + + /** @var IConfig */ + private $config; + + /** @var string */ + private const CONFIG_KEY = 'buildCalendarReminderIndex'; + + /** + * @param IDBConnection $db + * @param IJobList $jobList + * @param IConfig $config + */ + public function __construct(IDBConnection $db, + IJobList $jobList, + IConfig $config) { + $this->db = $db; + $this->jobList = $jobList; + $this->config = $config; + } + + /** + * @return string + */ + public function getName() { + return 'Registering building of calendar reminder index as background job'; + } + + /** + * @param IOutput $output + */ + public function run(IOutput $output) { + // only run once + if ($this->config->getAppValue('dav', self::CONFIG_KEY) === 'yes') { + $output->info('Repair step already executed'); + return; + } + + $query = $this->db->getQueryBuilder(); + $query->select($query->createFunction('MAX(' . $query->getColumnName('id') . ')')) + ->from('calendarobjects'); + $maxId = (int)$query->execute()->fetchColumn(); + + $output->info('Add background job'); + $this->jobList->add(BuildReminderIndexBackgroundJob::class, [ + 'offset' => 0, + 'stopAt' => $maxId + ]); + + // if all were done, no need to redo the repair during next upgrade + $this->config->setAppValue('dav', self::CONFIG_KEY, 'yes'); + } +} \ No newline at end of file diff --git a/apps/dav/lib/Migration/Version1012Date20190808122342.php b/apps/dav/lib/Migration/Version1012Date20190808122342.php new file mode 100644 index 0000000000..4aa768e705 --- /dev/null +++ b/apps/dav/lib/Migration/Version1012Date20190808122342.php @@ -0,0 +1,116 @@ + + * + * @author Georg Ehrke + * + * @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 . + * + */ +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 Version1012Date20190808122342 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):?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('calendar_reminders')) { + $table = $schema->createTable('calendar_reminders'); + + $table->addColumn('id', Type::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ]); + $table->addColumn('calendar_id', Type::BIGINT, [ + 'notnull' => true, + 'length' => 11, + ]); + $table->addColumn('object_id', Type::BIGINT, [ + 'notnull' => true, + 'length' => 11, + ]); + $table->addColumn('is_recurring', Type::SMALLINT, [ + 'notnull' => true, + 'length' => 1, + ]); + $table->addColumn('uid', Type::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn('recurrence_id', Type::BIGINT, [ + 'notnull' => false, + 'length' => 11, + 'unsigned' => true, + ]); + $table->addColumn('is_recurrence_exception', Type::SMALLINT, [ + 'notnull' => true, + 'length' => 1, + ]); + $table->addColumn('event_hash', Type::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn('alarm_hash', Type::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn('type', Type::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $table->addColumn('is_relative', Type::SMALLINT, [ + 'notnull' => true, + 'length' => 1, + ]); + $table->addColumn('notification_date', Type::BIGINT, [ + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ]); + $table->addColumn('is_repeat_based', Type::SMALLINT, [ + 'notnull' => true, + 'length' => 1, + ]); + + $table->setPrimaryKey(['id']); + $table->addIndex(['object_id'], 'calendar_reminder_objid'); + $table->addIndex(['uid', 'recurrence_id'], 'calendar_reminder_uidrec'); + + return $schema; + } + } +} diff --git a/apps/dav/lib/Settings/CalDAVSettings.php b/apps/dav/lib/Settings/CalDAVSettings.php index f38143b5b4..958c463b1d 100644 --- a/apps/dav/lib/Settings/CalDAVSettings.php +++ b/apps/dav/lib/Settings/CalDAVSettings.php @@ -48,6 +48,7 @@ class CalDAVSettings implements ISettings { $parameters = [ 'send_invitations' => $this->config->getAppValue('dav', 'sendInvitations', 'yes'), 'generate_birthday_calendar' => $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes'), + 'send_reminders_notifications' => $this->config->getAppValue('dav', 'sendEventReminders', 'yes'), ]; return new TemplateResponse('dav', 'settings-admin-caldav', $parameters); diff --git a/apps/dav/templates/settings-admin-caldav.php b/apps/dav/templates/settings-admin-caldav.php index 87b159923d..ba55a88470 100644 --- a/apps/dav/templates/settings-admin-caldav.php +++ b/apps/dav/templates/settings-admin-caldav.php @@ -72,4 +72,25 @@ script('dav', [ t('Birthday calendars will be generated by a background job.')); ?>
t('Hence they will not be available immediately after enabling but will show up after some time.')); ?>

+

+ /> + +
+ + ', + '', + ], + $l->t('Please make sure to properly set up {emailopen}the email server{linkclose}.') + )); ?> + +
+ t('Notifications will be send through background jobs, so these need to happen often enough.')); ?> +

diff --git a/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php b/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php new file mode 100644 index 0000000000..c3b4b7e54c --- /dev/null +++ b/apps/dav/tests/unit/BackgroundJob/EventReminderJobTest.php @@ -0,0 +1,93 @@ + + * + * @author Thomas Citharel + * + * @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 . + * + */ + +namespace OCA\DAV\Tests\unit\BackgroundJob; + +use OCA\DAV\BackgroundJob\EventReminderJob; +use OCA\DAV\CalDAV\Reminder\ReminderService; +use OCP\IConfig; +use Test\TestCase; + +class EventReminderJobTest extends TestCase { + + /** @var ReminderService|\PHPUnit\Framework\MockObject\MockObject */ + private $reminderService; + + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + private $config; + + /** @var EventReminderJob|\PHPUnit\Framework\MockObject\MockObject */ + private $backgroundJob; + + protected function setUp() { + parent::setUp(); + + $this->reminderService = $this->createMock(ReminderService::class); + $this->config = $this->createMock(IConfig::class); + + $this->backgroundJob = new EventReminderJob($this->reminderService, $this->config); + } + + public function data(): array + { + return [ + [true, true, true], + [true, false, false], + [false, true, false], + [false, false, false], + ]; + } + + /** + * @dataProvider data + * + * @param bool $sendEventReminders + * @param bool $sendEventRemindersMode + * @param bool $expectCall + */ + public function testRun(bool $sendEventReminders, bool $sendEventRemindersMode, bool $expectCall): void { + $this->config->expects($this->at(0)) + ->method('getAppValue') + ->with('dav', 'sendEventReminders', 'yes') + ->willReturn($sendEventReminders ? 'yes' : 'no'); + + if ($sendEventReminders) { + $this->config->expects($this->at(1)) + ->method('getAppValue') + ->with('dav', 'sendEventRemindersMode', 'backgroundjob') + ->willReturn($sendEventRemindersMode ? 'backgroundjob' : 'cron'); + + } + + if ($expectCall) { + $this->reminderService->expects($this->once()) + ->method('processReminders'); + } else { + $this->reminderService->expects($this->never()) + ->method('processReminders'); + } + + $this->backgroundJob->run([]); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php b/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php new file mode 100644 index 0000000000..71453ac73a --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php @@ -0,0 +1,397 @@ + + * @author Georg Ehrke + * + * @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 + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\AppFramework\Utility\ITimeFactory; +use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend; +use Test\TestCase; + +class BackendTest extends TestCase { + + /** + * Reminder Backend + * + * @var ReminderBackend|\PHPUnit\Framework\MockObject\MockObject + */ + private $reminderBackend; + + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + + public function setUp() { + parent::setUp(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->delete('calendar_reminders')->execute(); + $query->delete('calendarobjects')->execute(); + $query->delete('calendars')->execute(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->reminderBackend = new ReminderBackend(self::$realDatabase, $this->timeFactory); + + $this->createRemindersTestSet(); + } + + protected function tearDown() { + $query = self::$realDatabase->getQueryBuilder(); + $query->delete('calendar_reminders')->execute(); + $query->delete('calendarobjects')->execute(); + $query->delete('calendars')->execute(); + } + + + public function testCleanRemindersForEvent(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->reminderBackend->cleanRemindersForEvent(1); + + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(2, $rows); + } + + public function testCleanRemindersForCalendar(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->reminderBackend->cleanRemindersForCalendar(1); + + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(1, $rows); + } + + public function testRemoveReminder(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->reminderBackend->removeReminder((int) $rows[3]['id']); + + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(3, $rows); + } + + public function testGetRemindersToProcess(): void { + $this->timeFactory->expects($this->exactly(1)) + ->method('getTime') + ->with() + ->willReturn(123457); + + $rows = $this->reminderBackend->getRemindersToProcess(); + + $this->assertCount(2, $rows); + unset($rows[0]['id']); + unset($rows[1]['id']); + + $this->assertEquals($rows[0], [ + 'calendar_id' => 1, + 'object_id' => 1, + 'uid' => 'asd', + 'is_recurring' => false, + 'recurrence_id' => 123458, + 'is_recurrence_exception' => false, + 'event_hash' => 'asd123', + 'alarm_hash' => 'asd567', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 123456, + 'is_repeat_based' => false, + 'calendardata' => 'Calendar data 123', + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ]); + $this->assertEquals($rows[1], [ + 'calendar_id' => 1, + 'object_id' => 1, + 'uid' => 'asd', + 'is_recurring' => false, + 'recurrence_id' => 123458, + 'is_recurrence_exception' => false, + 'event_hash' => 'asd123', + 'alarm_hash' => 'asd567', + 'type' => 'AUDIO', + 'is_relative' => true, + 'notification_date' => 123456, + 'is_repeat_based' => false, + 'calendardata' => 'Calendar data 123', + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ]); + } + + public function testGetAllScheduledRemindersForEvent(): void { + $rows = $this->reminderBackend->getAllScheduledRemindersForEvent(1); + + $this->assertCount(2, $rows); + unset($rows[0]['id']); + unset($rows[1]['id']); + + $this->assertEquals($rows[0], [ + 'calendar_id' => 1, + 'object_id' => 1, + 'uid' => 'asd', + 'is_recurring' => false, + 'recurrence_id' => 123458, + 'is_recurrence_exception' => false, + 'event_hash' => 'asd123', + 'alarm_hash' => 'asd567', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 123456, + 'is_repeat_based' => false, + ]); + $this->assertEquals($rows[1], [ + 'calendar_id' => 1, + 'object_id' => 1, + 'uid' => 'asd', + 'is_recurring' => false, + 'recurrence_id' => 123458, + 'is_recurrence_exception' => false, + 'event_hash' => 'asd123', + 'alarm_hash' => 'asd567', + 'type' => 'AUDIO', + 'is_relative' => true, + 'notification_date' => 123456, + 'is_repeat_based' => false, + ]); + } + + public function testInsertReminder(): void { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->reminderBackend->insertReminder(42, 1337, 'uid99', true, 12345678, + true, 'hash99', 'hash42', 'AUDIO', false, 12345670, false); + + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(5, $rows); + + unset($rows[4]['id']); + + $this->assertEquals($rows[4], [ + 'calendar_id' => '42', + 'object_id' => '1337', + 'is_recurring' => '1', + 'uid' => 'uid99', + 'recurrence_id' => '12345678', + 'is_recurrence_exception' => '1', + 'event_hash' => 'hash99', + 'alarm_hash' => 'hash42', + 'type' => 'AUDIO', + 'is_relative' => '0', + 'notification_date' => '12345670', + 'is_repeat_based' => '0', + ]); + } + + public function testUpdateReminder() { + $query = self::$realDatabase->getQueryBuilder(); + $rows = $query->select('*') + ->from('calendar_reminders') + ->execute() + ->fetchAll(); + + $this->assertCount(4, $rows); + + $this->assertEquals($rows[3]['notification_date'], 123600); + + $reminderId = (int) $rows[3]['id']; + $newNotificationDate = 123700; + + $this->reminderBackend->updateReminder($reminderId, $newNotificationDate); + + $query = self::$realDatabase->getQueryBuilder(); + $row = $query->select('notification_date') + ->from('calendar_reminders') + ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) + ->execute() + ->fetch(); + + $this->assertEquals((int) $row['notification_date'], 123700); + } + + + private function createRemindersTestSet(): void { + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendars') + ->values([ + 'id' => $query->createNamedParameter(1), + 'principaluri' => $query->createNamedParameter('principals/users/user001'), + 'displayname' => $query->createNamedParameter('Displayname 123'), + ]) + ->execute(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendars') + ->values([ + 'id' => $query->createNamedParameter(99), + 'principaluri' => $query->createNamedParameter('principals/users/user002'), + 'displayname' => $query->createNamedParameter('Displayname 99'), + ]) + ->execute(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'id' => $query->createNamedParameter(1), + 'calendardata' => $query->createNamedParameter('Calendar data 123'), + 'calendarid' => $query->createNamedParameter(1), + 'size' => $query->createNamedParameter(42), + ]) + ->execute(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'id' => $query->createNamedParameter(2), + 'calendardata' => $query->createNamedParameter('Calendar data 456'), + 'calendarid' => $query->createNamedParameter(1), + 'size' => $query->createNamedParameter(42), + ]) + ->execute(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendarobjects') + ->values([ + 'id' => $query->createNamedParameter(10), + 'calendardata' => $query->createNamedParameter('Calendar data 789'), + 'calendarid' => $query->createNamedParameter(99), + 'size' => $query->createNamedParameter(42), + ]) + ->execute(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter(1), + 'object_id' => $query->createNamedParameter(1), + 'uid' => $query->createNamedParameter('asd'), + 'is_recurring' => $query->createNamedParameter(0), + 'recurrence_id' => $query->createNamedParameter(123458), + 'is_recurrence_exception' => $query->createNamedParameter(0), + 'event_hash' => $query->createNamedParameter('asd123'), + 'alarm_hash' => $query->createNamedParameter('asd567'), + 'type' => $query->createNamedParameter('EMAIL'), + 'is_relative' => $query->createNamedParameter(1), + 'notification_date' => $query->createNamedParameter(123456), + 'is_repeat_based' => $query->createNamedParameter(0), + ]) + ->execute(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter(1), + 'object_id' => $query->createNamedParameter(1), + 'uid' => $query->createNamedParameter('asd'), + 'is_recurring' => $query->createNamedParameter(0), + 'recurrence_id' => $query->createNamedParameter(123458), + 'is_recurrence_exception' => $query->createNamedParameter(0), + 'event_hash' => $query->createNamedParameter('asd123'), + 'alarm_hash' => $query->createNamedParameter('asd567'), + 'type' => $query->createNamedParameter('AUDIO'), + 'is_relative' => $query->createNamedParameter(1), + 'notification_date' => $query->createNamedParameter(123456), + 'is_repeat_based' => $query->createNamedParameter(0), + ]) + ->execute(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter(1), + 'object_id' => $query->createNamedParameter(2), + 'uid' => $query->createNamedParameter('asd'), + 'is_recurring' => $query->createNamedParameter(0), + 'recurrence_id' => $query->createNamedParameter(123900), + 'is_recurrence_exception' => $query->createNamedParameter(0), + 'event_hash' => $query->createNamedParameter('asd123'), + 'alarm_hash' => $query->createNamedParameter('asd567'), + 'type' => $query->createNamedParameter('EMAIL'), + 'is_relative' => $query->createNamedParameter(1), + 'notification_date' => $query->createNamedParameter(123499), + 'is_repeat_based' => $query->createNamedParameter(0), + ]) + ->execute(); + + $query = self::$realDatabase->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'calendar_id' => $query->createNamedParameter(99), + 'object_id' => $query->createNamedParameter(10), + 'uid' => $query->createNamedParameter('asd'), + 'is_recurring' => $query->createNamedParameter(0), + 'recurrence_id' => $query->createNamedParameter(123900), + 'is_recurrence_exception' => $query->createNamedParameter(0), + 'event_hash' => $query->createNamedParameter('asd123'), + 'alarm_hash' => $query->createNamedParameter('asd567'), + 'type' => $query->createNamedParameter('DISPLAY'), + 'is_relative' => $query->createNamedParameter(1), + 'notification_date' => $query->createNamedParameter(123600), + 'is_repeat_based' => $query->createNamedParameter(0), + ]) + ->execute(); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTest.php new file mode 100644 index 0000000000..5a8d328ef8 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AbstractNotificationProviderTest.php @@ -0,0 +1,89 @@ + + * + * @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 + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\AbstractProvider; +use OCP\IConfig; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\L10N\IFactory as L10NFactory; +use OCP\IUser; +use Test\TestCase; +use Sabre\VObject\Component\VCalendar; + +abstract class AbstractNotificationProviderTest extends TestCase { + + /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */ + protected $logger; + + /** @var L10NFactory|\PHPUnit\Framework\MockObject\MockObject */ + protected $l10nFactory; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + protected $l10n; + + /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ + protected $urlGenerator; + + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + protected $config; + + /** @var AbstractProvider|\PHPUnit\Framework\MockObject\MockObject */ + protected $provider; + + /** + * @var VCalendar + */ + protected $vcalendar; + + /** + * @var string + */ + protected $calendarDisplayName; + + /** + * @var IUser|\PHPUnit\Framework\MockObject\MockObject + */ + protected $user; + + public function setUp() { + parent::setUp(); + + $this->logger = $this->createMock(ILogger::class); + $this->l10nFactory = $this->createMock(L10NFactory::class); + $this->l10n = $this->createMock(IL10N::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->config = $this->createMock(IConfig::class); + + $this->vcalendar = new VCalendar(); + $this->vcalendar->add('VEVENT', [ + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800, + 'UID' => 'uid1234', + ]); + $this->calendarDisplayName = 'Personal'; + + $this->user = $this->createMock(IUser::class); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php new file mode 100644 index 0000000000..9938b2f732 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/AudioProviderTest.php @@ -0,0 +1,33 @@ + + * + * @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 + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider; + +class AudioProviderTest extends PushProviderTest { + + public function testNotificationType():void { + $this->assertEquals(AudioProvider::NOTIFICATION_TYPE, 'AUDIO'); + } + +} \ No newline at end of file diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php new file mode 100644 index 0000000000..9bf2957e9f --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php @@ -0,0 +1,546 @@ + + * @author Georg Ehrke + * + * @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 + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider; +use OCP\IConfig; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\L10N\IFactory as L10NFactory; +use OCP\IUser; +use OCP\Mail\IEMailTemplate; +use OCP\Mail\IMailer; +use OCP\Mail\IAttachment; +use OCP\Mail\IMessage; +use Sabre\VObject\Component\VCalendar; +use Test\TestCase; + +class EmailProviderTest extends AbstractNotificationProviderTest { + + const USER_EMAIL = 'frodo@hobb.it'; + + /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */ + protected $logger; + + /** @var L10NFactory|\PHPUnit\Framework\MockObject\MockObject */ + protected $l10nFactory; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + protected $l10n; + + /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ + protected $urlGenerator; + + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + protected $config; + + /** @var IMailer|\PHPUnit\Framework\MockObject\MockObject */ + private $mailer; + + public function setUp() { + parent::setUp(); + + $this->mailer = $this->createMock(IMailer::class); + + $this->provider = new EmailProvider( + $this->config, + $this->mailer, + $this->logger, + $this->l10nFactory, + $this->urlGenerator + ); + } + + public function testSendWithoutAttendees():void { + $user1 = $this->createMock(IUser::class); + $user1->method('getUID') + ->willReturn('uid1'); + $user1->method('getEMailAddress') + ->willReturn('uid1@example.com'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID') + ->willReturn('uid2'); + $user2->method('getEMailAddress') + ->willReturn('uid2@example.com'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID') + ->willReturn('uid3'); + $user3->method('getEMailAddress') + ->willReturn('uid3@example.com'); + $user4 = $this->createMock(IUser::class); + $user4->method('getUID') + ->willReturn('uid4'); + $user4->method('getEMailAddress') + ->willReturn(null); + + $users = [$user1, $user2, $user3, $user4]; + + $this->config->expects($this->at(0)) + ->method('getUserValue') + ->with('uid1', 'core', 'lang', null) + ->willReturn(null); + $this->config->expects($this->at(1)) + ->method('getUserValue') + ->with('uid2', 'core', 'lang', null) + ->willReturn('de'); + $this->config->expects($this->at(2)) + ->method('getUserValue') + ->with('uid3', 'core', 'lang', null) + ->willReturn('de'); + + $enL10N = $this->createMock(IL10N::class); + $enL10N->method('t') + ->will($this->returnArgument(0)); + $enL10N->method('l') + ->will($this->returnArgument(0)); + + $deL10N = $this->createMock(IL10N::class); + $deL10N->method('t') + ->will($this->returnArgument(0)); + $deL10N->method('l') + ->will($this->returnArgument(0)); + + $this->l10nFactory->expects($this->at(0)) + ->method('findLanguage') + ->with() + ->willReturn('en'); + + $this->l10nFactory->expects($this->at(1)) + ->method('languageExists') + ->with('dav', 'en') + ->willReturn(true); + + $this->l10nFactory->expects($this->at(2)) + ->method('get') + ->with('dav', 'en') + ->willReturn($enL10N); + + $this->l10nFactory->expects($this->at(3)) + ->method('languageExists') + ->with('dav', 'de') + ->willReturn(true); + + $this->l10nFactory->expects($this->at(4)) + ->method('get') + ->with('dav', 'de') + ->willReturn($deL10N); + + $template1 = $this->getTemplateMock(); + $message11 = $this->getMessageMock('uid1@example.com', $template1); + $template2 = $this->getTemplateMock(); + $message21 = $this->getMessageMock('uid2@example.com', $template2); + $message22 = $this->getMessageMock('uid3@example.com', $template2); + + $this->mailer->expects($this->at(0)) + ->method('createEMailTemplate') + ->with('dav.calendarReminder') + ->willReturn($template1); + + $this->mailer->expects($this->at(1)) + ->method('createMessage') + ->with() + ->willReturn($message11); + $this->mailer->expects($this->at(2)) + ->method('send') + ->with($message11) + ->willReturn([]); + + $this->mailer->expects($this->at(3)) + ->method('createEMailTemplate') + ->with('dav.calendarReminder') + ->willReturn($template2); + + $this->mailer->expects($this->at(4)) + ->method('createMessage') + ->with() + ->willReturn($message21); + $this->mailer->expects($this->at(5)) + ->method('send') + ->with($message21) + ->willReturn([]); + $this->mailer->expects($this->at(6)) + ->method('createMessage') + ->with() + ->willReturn($message22); + $this->mailer->expects($this->at(7)) + ->method('send') + ->with($message22) + ->willReturn([]); + + $this->setupURLGeneratorMock(2); + + $vcalendar = $this->getNoAttendeeVCalendar(); + $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $users); + } + + public function testSendWithAttendees(): void { + $user1 = $this->createMock(IUser::class); + $user1->method('getUID') + ->willReturn('uid1'); + $user1->method('getEMailAddress') + ->willReturn('uid1@example.com'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID') + ->willReturn('uid2'); + $user2->method('getEMailAddress') + ->willReturn('uid2@example.com'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID') + ->willReturn('uid3'); + $user3->method('getEMailAddress') + ->willReturn('uid3@example.com'); + $user4 = $this->createMock(IUser::class); + $user4->method('getUID') + ->willReturn('uid4'); + $user4->method('getEMailAddress') + ->willReturn(null); + + $users = [$user1, $user2, $user3, $user4]; + + $this->config->expects($this->at(0)) + ->method('getUserValue') + ->with('uid1', 'core', 'lang', null) + ->willReturn(null); + $this->config->expects($this->at(1)) + ->method('getUserValue') + ->with('uid2', 'core', 'lang', null) + ->willReturn('de'); + $this->config->expects($this->at(2)) + ->method('getUserValue') + ->with('uid3', 'core', 'lang', null) + ->willReturn('de'); + + $enL10N = $this->createMock(IL10N::class); + $enL10N->method('t') + ->will($this->returnArgument(0)); + $enL10N->method('l') + ->will($this->returnArgument(0)); + + $deL10N = $this->createMock(IL10N::class); + $deL10N->method('t') + ->will($this->returnArgument(0)); + $deL10N->method('l') + ->will($this->returnArgument(0)); + + $this->l10nFactory->expects($this->at(0)) + ->method('findLanguage') + ->with() + ->willReturn('en'); + + $this->l10nFactory->expects($this->at(1)) + ->method('languageExists') + ->with('dav', 'de') + ->willReturn(true); + + $this->l10nFactory->expects($this->at(2)) + ->method('get') + ->with('dav', 'de') + ->willReturn($enL10N); + + $this->l10nFactory->expects($this->at(3)) + ->method('languageExists') + ->with('dav', 'en') + ->willReturn(true); + + $this->l10nFactory->expects($this->at(4)) + ->method('get') + ->with('dav', 'en') + ->willReturn($deL10N); + + $template1 = $this->getTemplateMock(); + $message11 = $this->getMessageMock('foo1@example.org', $template1); + $message12 = $this->getMessageMock('uid2@example.com', $template1); + $message13 = $this->getMessageMock('uid3@example.com', $template1); + $template2 = $this->getTemplateMock(); + $message21 = $this->getMessageMock('foo3@example.org', $template2); + $message22 = $this->getMessageMock('foo4@example.org', $template2); + $message23 = $this->getMessageMock('uid1@example.com', $template2); + + $this->mailer->expects($this->at(0)) + ->method('createEMailTemplate') + ->with('dav.calendarReminder') + ->willReturn($template1); + + $this->mailer->expects($this->at(1)) + ->method('createMessage') + ->with() + ->willReturn($message11); + $this->mailer->expects($this->at(2)) + ->method('send') + ->with($message11) + ->willReturn([]); + $this->mailer->expects($this->at(3)) + ->method('createMessage') + ->with() + ->willReturn($message12); + $this->mailer->expects($this->at(4)) + ->method('send') + ->with($message12) + ->willReturn([]); + $this->mailer->expects($this->at(5)) + ->method('createMessage') + ->with() + ->willReturn($message13); + $this->mailer->expects($this->at(6)) + ->method('send') + ->with($message13) + ->willReturn([]); + + $this->mailer->expects($this->at(7)) + ->method('createEMailTemplate') + ->with('dav.calendarReminder') + ->willReturn($template2); + + $this->mailer->expects($this->at(8)) + ->method('createMessage') + ->with() + ->willReturn($message21); + $this->mailer->expects($this->at(9)) + ->method('send') + ->with($message21) + ->willReturn([]); + $this->mailer->expects($this->at(10)) + ->method('createMessage') + ->with() + ->willReturn($message22); + $this->mailer->expects($this->at(11)) + ->method('send') + ->with($message22) + ->willReturn([]); + $this->mailer->expects($this->at(12)) + ->method('createMessage') + ->with() + ->willReturn($message23); + $this->mailer->expects($this->at(13)) + ->method('send') + ->with($message23) + ->willReturn([]); + + $this->setupURLGeneratorMock(2); + + $vcalendar = $this->getAttendeeVCalendar(); + $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $users); + } + + /** + * @return IEMailTemplate + */ + private function getTemplateMock():IEMailTemplate { + $template = $this->createMock(IEMailTemplate::class); + + $template->expects($this->at(0)) + ->method('addHeader') + ->with() + ->willReturn($template); + + $template->expects($this->at(1)) + ->method('setSubject') + ->with() + ->willReturn($template); + + $template->expects($this->at(2)) + ->method('addHeading') + ->with() + ->willReturn($template); + + $template->expects($this->at(3)) + ->method('addBodyListItem') + ->with() + ->willReturn($template); + + $template->expects($this->at(4)) + ->method('addBodyListItem') + ->with() + ->willReturn($template); + + $template->expects($this->at(5)) + ->method('addBodyListItem') + ->with() + ->willReturn($template); + + $template->expects($this->at(6)) + ->method('addBodyListItem') + ->with() + ->willReturn($template); + + $template->expects($this->at(7)) + ->method('addFooter') + ->with() + ->willReturn($template); + + return $template; + } + + /** + * @param array $toMail + * @param IEMailTemplate $templateMock + * @param array $replyTo + * @return IMessage + */ + private function getMessageMock(string $toMail, IEMailTemplate $templateMock, array $replyTo=null):IMessage { + $message = $this->createMock(IMessage::class); + $i = 0; + + $message->expects($this->at($i++)) + ->method('setFrom') + ->with([\OCP\Util::getDefaultEmailAddress('reminders-noreply')]) + ->willReturn($message); + + if ($replyTo) { + $message->expects($this->at($i++)) + ->method('setReplyTo') + ->with($replyTo) + ->willReturn($message); + } + + $message->expects($this->at($i++)) + ->method('setTo') + ->with([$toMail]) + ->willReturn($message); + + $message->expects($this->at($i++)) + ->method('useTemplate') + ->with($templateMock) + ->willReturn($message); + + return $message; + } + + private function getNoAttendeeVCalendar():VCalendar { + $vcalendar = new VCalendar(); + $vcalendar->add('VEVENT', [ + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800, + 'UID' => 'uid1234', + 'LOCATION' => 'Location 123', + 'DESCRIPTION' => 'DESCRIPTION 456', + ]); + + return $vcalendar; + } + + private function getAttendeeVCalendar():VCalendar { + $vcalendar = new VCalendar(); + $vcalendar->add('VEVENT', [ + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2017-01-01 00:00:00+00:00'), // 1483228800, + 'UID' => 'uid1234', + 'LOCATION' => 'Location 123', + 'DESCRIPTION' => 'DESCRIPTION 456', + ]); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'mailto:foo1@example.org', + [ + 'LANG' => 'de', + 'PARTSTAT' => 'NEEDS-ACTION', + ] + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'mailto:foo2@example.org', + [ + 'LANG' => 'de', + 'PARTSTAT' => 'DECLINED', + ] + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'mailto:foo3@example.org', + [ + 'LANG' => 'en', + 'PARTSTAT' => 'CONFIRMED', + ] + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'mailto:foo4@example.org' + ); + + $vcalendar->VEVENT->add( + 'ATTENDEE', + 'tomail:foo5@example.org' + ); + + return $vcalendar; + } + + private function setupURLGeneratorMock(int $times=1):void { + for ($i = 0; $i < $times; $i++) { + $this->urlGenerator + ->expects($this->at(8 * $i)) + ->method('imagePath') + ->with('core', 'actions/info.svg') + ->willReturn('imagePath1'); + + $this->urlGenerator + ->expects($this->at(8 * $i + 1)) + ->method('getAbsoluteURL') + ->with('imagePath1') + ->willReturn('AbsURL1'); + + $this->urlGenerator + ->expects($this->at(8 * $i + 2)) + ->method('imagePath') + ->with('core', 'places/calendar.svg') + ->willReturn('imagePath2'); + + $this->urlGenerator + ->expects($this->at(8 * $i + 3)) + ->method('getAbsoluteURL') + ->with('imagePath2') + ->willReturn('AbsURL2'); + + $this->urlGenerator + ->expects($this->at(8 * $i + 4)) + ->method('imagePath') + ->with('core', 'actions/address.svg') + ->willReturn('imagePath3'); + + $this->urlGenerator + ->expects($this->at(8 * $i + 5)) + ->method('getAbsoluteURL') + ->with('imagePath3') + ->willReturn('AbsURL3'); + + $this->urlGenerator + ->expects($this->at(8 * $i + 6)) + ->method('imagePath') + ->with('core', 'actions/more.svg') + ->willReturn('imagePath4'); + + $this->urlGenerator + ->expects($this->at(8 * $i + 7)) + ->method('getAbsoluteURL') + ->with('imagePath4') + ->willReturn('AbsURL4'); + } + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php new file mode 100644 index 0000000000..bbf71837b0 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php @@ -0,0 +1,186 @@ + + * @author Georg Ehrke + * + * @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 + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder\NotificationProvider; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider; +use OCP\IConfig; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\L10N\IFactory as L10NFactory; +use OCP\IUser; +use OCP\Notification\IManager; +use OCP\Notification\INotification; +use OCP\AppFramework\Utility\ITimeFactory; +use Test\TestCase; + +class PushProviderTest extends AbstractNotificationProviderTest { + + /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */ + protected $logger; + + /** @var L10NFactory|\PHPUnit\Framework\MockObject\MockObject */ + protected $l10nFactory; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + protected $l10n; + + /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ + protected $urlGenerator; + + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + protected $config; + + /** @var IManager|\PHPUnit\Framework\MockObject\MockObject */ + private $manager; + + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + + public function setUp() { + parent::setUp(); + + $this->manager = $this->createMock(IManager::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->provider = new PushProvider( + $this->config, + $this->manager, + $this->logger, + $this->l10nFactory, + $this->urlGenerator, + $this->timeFactory + ); + } + + public function testNotificationType():void { + $this->assertEquals(PushProvider::NOTIFICATION_TYPE, 'DISPLAY'); + } + + public function testSend(): void { + $user1 = $this->createMock(IUser::class); + $user1->method('getUID') + ->willReturn('uid1'); + $user2 = $this->createMock(IUser::class); + $user2->method('getUID') + ->willReturn('uid2'); + $user3 = $this->createMock(IUser::class); + $user3->method('getUID') + ->willReturn('uid3'); + + $users = [$user1, $user2, $user3]; + + $dateTime = new \DateTime('@946684800'); + $this->timeFactory->method('getDateTime') + ->with() + ->willReturn($dateTime); + + $notification1 = $this->createNotificationMock('uid1', $dateTime); + $notification2 = $this->createNotificationMock('uid2', $dateTime); + $notification3 = $this->createNotificationMock('uid3', $dateTime); + + $this->manager->expects($this->at(0)) + ->method('createNotification') + ->with() + ->willReturn($notification1); + $this->manager->expects($this->at(2)) + ->method('createNotification') + ->with() + ->willReturn($notification2); + $this->manager->expects($this->at(4)) + ->method('createNotification') + ->with() + ->willReturn($notification3); + + $this->manager->expects($this->at(1)) + ->method('notify') + ->with($notification1); + $this->manager->expects($this->at(3)) + ->method('notify') + ->with($notification2); + $this->manager->expects($this->at(5)) + ->method('notify') + ->with($notification3); + + $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, $users); + } + + /** + * @param string $uid + * @param \DateTime $dt + */ + private function createNotificationMock(string $uid, \DateTime $dt):INotification { + $notification = $this->createMock(INotification::class); + $notification + ->expects($this->once()) + ->method('setApp') + ->with('dav') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setUser') + ->with($uid) + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setDateTime') + ->with($dt) + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setObject') + ->with('dav', 'uid1234') + ->willReturn($notification); + + $notification->expects($this->once()) + ->method('setSubject') + ->with('calendar_reminder', [ + 'title' => 'Fellowship meeting', + 'start_atom' => '2017-01-01T00:00:00+00:00', + ]) + ->willReturn($notification); + + $notification + ->expects($this->once()) + ->method('setMessage') + ->with('calendar_reminder', [ + 'title' => 'Fellowship meeting', + 'start_atom' => '2017-01-01T00:00:00+00:00', + 'description' => null, + 'location' => null, + 'all_day' => false, + 'start_is_floating' => false, + 'start_timezone' => 'UTC', + 'end_atom' => '2017-01-01T00:00:00+00:00', + 'end_is_floating' => false, + 'end_timezone' => 'UTC', + 'calendar_displayname' => 'Personal', + ]) + ->willReturn($notification); + + return $notification; + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php new file mode 100644 index 0000000000..b4c62eacd7 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotificationProviderManagerTest.php @@ -0,0 +1,94 @@ + + * @author Georg Ehrke + * + * @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 + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder; + +use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProviderManager; +use OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException; +use OCA\DAV\Capabilities; +use Test\TestCase; + +class NotificationProviderManagerTest extends TestCase { + + /** @var NotificationProviderManager|\PHPUnit\Framework\MockObject\MockObject */ + private $providerManager; + + /** + * @throws \OCP\AppFramework\QueryException + */ + public function setUp() { + parent::setUp(); + + $this->providerManager = new NotificationProviderManager(); + $this->providerManager->registerProvider(EmailProvider::class); + } + + /** + * @expectedException OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException + * @expectedExceptionMessage Type NOT EXISTENT is not an accepted type of notification + * @throws ProviderNotAvailableException + * @throws NotificationTypeDoesNotExistException + */ + public function testGetProviderForUnknownType(): void{ + $this->providerManager->getProvider('NOT EXISTENT'); + } + + /** + * @expectedException OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException + * @expectedExceptionMessage No notification provider for type AUDIO available + * @throws NotificationTypeDoesNotExistException + * @throws ProviderNotAvailableException + */ + public function testGetProviderForUnRegisteredType(): void{ + $this->providerManager->getProvider('AUDIO'); + } + + public function testGetProvider(): void{ + $provider = $this->providerManager->getProvider('EMAIL'); + $this->assertInstanceOf(EmailProvider::class, $provider); + } + + public function testRegisterProvider(): void{ + $this->providerManager->registerProvider(PushProvider::class); + $provider = $this->providerManager->getProvider('DISPLAY'); + $this->assertInstanceOf(PushProvider::class, $provider); + } + + /** + * @expectedExceptionMessage Invalid notification provider registered + * @expectedException \InvalidArgumentException + * @throws \OCP\AppFramework\QueryException + */ + public function testRegisterBadProvider(): void{ + $this->providerManager->registerProvider(Capabilities::class); + } + + public function testHasProvider(): void { + $this->assertTrue($this->providerManager->hasProvider('EMAIL')); + $this->assertFalse($this->providerManager->hasProvider('EMAIL123')); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php b/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php new file mode 100644 index 0000000000..8d38617ad2 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/NotifierTest.php @@ -0,0 +1,220 @@ + + * @author Georg Ehrke + * + * @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 + * + */ + +namespace OCA\DAV\Tests\unit\CalDAV\Reminder; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\Reminder\Notifier; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; +use OCP\Notification\INotification; +use Test\TestCase; + +class NotifierTest extends TestCase { + /** @var Notifier */ + protected $notifier; + + /** @var IFactory|\PHPUnit\Framework\MockObject\MockObject */ + protected $factory; + + /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */ + protected $urlGenerator; + + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + protected $l10n; + + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + protected $timeFactory; + + protected function setUp() { + parent::setUp(); + + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->l10n = $this->createMock(IL10N::class); + $this->l10n->expects($this->any()) + ->method('t') + ->willReturnCallback(function($string, $args) { + return vsprintf($string, $args); + }); + $this->l10n->expects($this->any()) + ->method('l') + ->willReturnCallback(function($string, $args) { + /** \DateTime $args */ + return $args->format(\DateTime::ATOM); + }); + $this->l10n->expects($this->any()) + ->method('n') + ->willReturnCallback(function($textSingular, $textPlural, $count, $args) { + $text = $count === 1 ? $textSingular : $textPlural; + $text = str_replace('%n', (string)$count, $text); + return vsprintf($text, $args); + }); + $this->factory = $this->createMock(IFactory::class); + $this->factory->expects($this->any()) + ->method('get') + ->willReturn($this->l10n); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->timeFactory + ->method('getDateTime') + ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2005-08-15T14:00:00+02:00')); + + $this->notifier = new Notifier( + $this->factory, + $this->urlGenerator, + $this->timeFactory + ); + } + + public function testGetId():void { + $this->assertEquals($this->notifier->getID(), 'dav'); + } + + public function testGetName():void { + $this->assertEquals($this->notifier->getName(), 'Calendar'); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Notification not from this app + */ + public function testPrepareWrongApp(): void + { + /** @var INotification|\PHPUnit\Framework\MockObject\MockObject $notification */ + $notification = $this->createMock(INotification::class); + + $notification->expects($this->once()) + ->method('getApp') + ->willReturn('notifications'); + $notification->expects($this->never()) + ->method('getSubject'); + + $this->notifier->prepare($notification, 'en'); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unknown subject + */ + public function testPrepareWrongSubject() { + /** @var INotification|\PHPUnit\Framework\MockObject\MockObject $notification */ + $notification = $this->createMock(INotification::class); + + $notification->expects($this->once()) + ->method('getApp') + ->willReturn(Application::APP_ID); + $notification->expects($this->once()) + ->method('getSubject') + ->willReturn('wrong subject'); + + $this->notifier->prepare($notification, 'en'); + } + + public function dataPrepare(): array + { + return [ + [ + 'calendar_reminder', + [ + 'title' => 'Title of this event', + 'start_atom' => '2005-08-15T15:52:01+02:00' + ], + 'Title of this event (in 1 hour, 52 minutes)', + [ + 'title' => 'Title of this event', + 'description' => null, + 'location' => 'NC Headquarters', + 'all_day' => false, + 'start_atom' => '2005-08-15T15:52:01+02:00', + 'start_is_floating' => false, + 'start_timezone' => 'Europe/Berlin', + 'end_atom' => '2005-08-15T17:52:01+02:00', + 'end_is_floating' => false, + 'end_timezone' => 'Europe/Berlin', + 'calendar_displayname' => 'Personal', + ], + "Calendar: Personal\r\nDate: 2005-08-15T15:52:01+02:00, 2005-08-15T15:52:01+02:00 - 2005-08-15T17:52:01+02:00 (Europe/Berlin)\r\nWhere: NC Headquarters" + ], + ]; + } + + /** + * @dataProvider dataPrepare + * + * @param string $subjectType + * @param array $subjectParams + * @param string $subject + * @param array $messageParams + * @param string $message + * @throws \Exception + */ + public function testPrepare(string $subjectType, array $subjectParams, string $subject, array $messageParams, string $message): void + { + /** @var INotification|\PHPUnit\Framework\MockObject\MockObject $notification */ + $notification = $this->createMock(INotification::class); + + $notification->expects($this->once()) + ->method('getApp') + ->willReturn(Application::APP_ID); + $notification->expects($this->once()) + ->method('getSubject') + ->willReturn($subjectType); + $notification->expects($this->once()) + ->method('getSubjectParameters') + ->willReturn($subjectParams); + $notification->expects($this->once()) + ->method('getMessageParameters') + ->willReturn($messageParams); + + $notification->expects($this->once()) + ->method('setParsedSubject') + ->with($subject) + ->willReturnSelf(); + + $notification->expects($this->once()) + ->method('setParsedMessage') + ->with($message) + ->willReturnSelf(); + + $this->urlGenerator->expects($this->once()) + ->method('imagePath') + ->with('core', 'places/calendar.svg') + ->willReturn('icon-url'); + $this->urlGenerator->expects($this->once()) + ->method('getAbsoluteURL') + ->with('icon-url') + ->willReturn('absolute-icon-url'); + $notification->expects($this->once()) + ->method('setIcon') + ->with('absolute-icon-url') + ->willReturnSelf(); + + $return = $this->notifier->prepare($notification, 'en'); + + $this->assertEquals($notification, $return); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php new file mode 100644 index 0000000000..a9acca66a8 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php @@ -0,0 +1,555 @@ + + * + * @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 + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\Reminder; + +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Reminder\AbstractNotificationProvider; +use OCA\DAV\CalDAV\Reminder\Backend; +use OCA\DAV\CalDAV\Reminder\INotificationProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProviderManager; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider; +use OCA\DAV\CalDAV\Reminder\ReminderService; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use Test\TestCase; + +class ReminderServiceTest extends TestCase { + + /** @var Backend|\PHPUnit\Framework\MockObject\MockObject */ + private $backend; + + /** @var NotificationProviderManager|\PHPUnit\Framework\MockObject\MockObject */ + private $notificationProviderManager; + + /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ + private $userManager; + + /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject*/ + private $groupManager; + + /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */ + private $userSession; + + /** @var CalDavBackend|\PHPUnit\Framework\MockObject\MockObject */ + private $caldavBackend; + + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + + /** @var ReminderService */ + private $reminderService; + + public const CALENDAR_DATA = <<backend = $this->createMock(Backend::class); + $this->notificationProviderManager = $this->createMock(NotificationProviderManager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->groupManager = $this->createMock(IGroupManager::class); + $this->caldavBackend = $this->createMock(CalDavBackend::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->caldavBackend->method('getShares')->willReturn([]); + + $this->reminderService = new ReminderService($this->backend, + $this->notificationProviderManager, + $this->userManager, + $this->groupManager, + $this->caldavBackend, + $this->timeFactory); + } + + public function testOnCalendarObjectDelete():void { + $this->backend->expects($this->once()) + ->method('cleanRemindersForEvent') + ->with(44); + + $action = '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject'; + $objectData = [ + 'id' => '44', + 'component' => 'vevent', + ]; + + $this->reminderService->onTouchCalendarObject($action, $objectData); + } + + public function testOnCalendarObjectCreateSingleEntry():void { + $action = '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'; + $objectData = [ + 'calendardata' => self::CALENDAR_DATA, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $this->backend->expects($this->exactly(2)) + ->method('insertReminder') + ->withConsecutive( + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1465429500, false], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', '35b3eae8e792aa2209f0b4e1a302f105', 'DISPLAY', false, 1465344000, false] + ) + ->willReturn(1); + + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + + $this->reminderService->onTouchCalendarObject($action, $objectData); + } + + public function testOnCalendarObjectCreateSingleEntryWithRepeat(): void { + $action = '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'; + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_REPEAT, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $this->backend->expects($this->exactly(5)) + ->method('insertReminder') + ->withConsecutive( + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429500, false], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429620, true], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429740, true], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429860, true], + [1337, 42, 'wej2z68l9h', false, 1465430400, false, '5c70531aab15c92b52518ae10a2f78a4', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1465429980, true] + ) + ->willReturn(1); + + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + + $this->reminderService->onTouchCalendarObject($action, $objectData); + } + + public function testOnCalendarObjectCreateRecurringEntry(): void { + $action = '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'; + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_RECURRING, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $this->backend->expects($this->exactly(2)) + ->method('insertReminder') + ->withConsecutive( + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'de919af7429d3b5c11e8b9d289b411a6', 'EMAIL', true, 1467243900, false], + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false] + ) + ->willReturn(1); + + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-29T00:00:00+00:00')); + + $this->reminderService->onTouchCalendarObject($action, $objectData); + } + + public function testOnCalendarObjectCreateRecurringEntryWithRepeat():void { + $action = '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject'; + $objectData = [ + 'calendardata' => self::CALENDAR_DATA_RECURRING_REPEAT, + 'id' => '42', + 'calendarid' => '1337', + 'component' => 'vevent', + ]; + + $this->backend->expects($this->exactly(6)) + ->method('insertReminder') + ->withConsecutive( + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467243900, false], + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244020, true], + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244140, true], + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244260, true], + [1337, 42, 'wej2z68l9h', true, 1467244800, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467244380, true], + [1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467158400, false] + ) + ->willReturn(1); + + $this->timeFactory->expects($this->once()) + ->method('getDateTime') + ->with() + ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-29T00:00:00+00:00')); + + $this->reminderService->onTouchCalendarObject($action, $objectData); + } + + public function testProcessReminders():void { + $this->backend->expects($this->at(0)) + ->method('getRemindersToProcess') + ->with() + ->willReturn([ + [ + 'id' => 1, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => false, + 'recurrence_id' => 1465430400, + 'is_recurrence_exception' => false, + 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4', + 'alarm_hash' => 'de919af7429d3b5c11e8b9d289b411a6', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 1465429500, + 'is_repeat_based' => false, + 'calendardata' => self::CALENDAR_DATA, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ], + [ + 'id' => 2, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => false, + 'recurrence_id' => 1465430400, + 'is_recurrence_exception' => false, + 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4', + 'alarm_hash' => 'ecacbf07d413c3c78d1ac7ad8c469602', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 1465429740, + 'is_repeat_based' => true, + 'calendardata' => self::CALENDAR_DATA_REPEAT, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ], + [ + 'id' => 3, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => false, + 'recurrence_id' => 1465430400, + 'is_recurrence_exception' => false, + 'event_hash' => '5c70531aab15c92b52518ae10a2f78a4', + 'alarm_hash' => '35b3eae8e792aa2209f0b4e1a302f105', + 'type' => 'DISPLAY', + 'is_relative' => false, + 'notification_date' => 1465344000, + 'is_repeat_based' => false, + 'calendardata' => self::CALENDAR_DATA, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ], + [ + 'id' => 4, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => true, + 'recurrence_id' => 1467244800, + 'is_recurrence_exception' => false, + 'event_hash' => 'fbdb2726bc0f7dfacac1d881c1453e20', + 'alarm_hash' => 'ecacbf07d413c3c78d1ac7ad8c469602', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 1467243900, + 'is_repeat_based' => false, + 'calendardata' => self::CALENDAR_DATA_RECURRING_REPEAT, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ], + [ + 'id' => 5, + 'calendar_id' => 1337, + 'object_id' => 42, + 'uid' => 'wej2z68l9h', + 'is_recurring' => true, + 'recurrence_id' => 1467849600, + 'is_recurrence_exception' => false, + 'event_hash' => 'fbdb2726bc0f7dfacac1d881c1453e20', + 'alarm_hash' => '8996992118817f9f311ac5cc56d1cc97', + 'type' => 'EMAIL', + 'is_relative' => true, + 'notification_date' => 1467158400, + 'is_repeat_based' => false, + 'calendardata' => self::CALENDAR_DATA_RECURRING, + 'displayname' => 'Displayname 123', + 'principaluri' => 'principals/users/user001', + ] + ]); + + $this->notificationProviderManager->expects($this->at(0)) + ->method('hasProvider') + ->with('EMAIL') + ->willReturn(true); + + $provider1 = $this->createMock(INotificationProvider::class); + $this->notificationProviderManager->expects($this->at(1)) + ->method('getProvider') + ->with('EMAIL') + ->willReturn($provider1); + + $this->notificationProviderManager->expects($this->at(2)) + ->method('hasProvider') + ->with('EMAIL') + ->willReturn(true); + + $provider2 = $this->createMock(INotificationProvider::class); + $this->notificationProviderManager->expects($this->at(3)) + ->method('getProvider') + ->with('EMAIL') + ->willReturn($provider2); + + $this->notificationProviderManager->expects($this->at(4)) + ->method('hasProvider') + ->with('DISPLAY') + ->willReturn(true); + + $provider3 = $this->createMock(INotificationProvider::class); + $this->notificationProviderManager->expects($this->at(5)) + ->method('getProvider') + ->with('DISPLAY') + ->willReturn($provider3); + + $this->notificationProviderManager->expects($this->at(6)) + ->method('hasProvider') + ->with('EMAIL') + ->willReturn(true); + + $provider4 = $this->createMock(INotificationProvider::class); + $this->notificationProviderManager->expects($this->at(7)) + ->method('getProvider') + ->with('EMAIL') + ->willReturn($provider4); + + $this->notificationProviderManager->expects($this->at(8)) + ->method('hasProvider') + ->with('EMAIL') + ->willReturn(true); + + $provider5 = $this->createMock(INotificationProvider::class); + $this->notificationProviderManager->expects($this->at(9)) + ->method('getProvider') + ->with('EMAIL') + ->willReturn($provider5); + + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->exactly(5)) + ->method('get') + ->with('user001') + ->willReturn($user); + + $provider1->expects($this->once()) + ->method('send') + ->with($this->callback(function($vevent) { + if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + $provider2->expects($this->once()) + ->method('send') + ->with($this->callback(function($vevent) { + if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + $provider3->expects($this->once()) + ->method('send') + ->with($this->callback(function($vevent) { + if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-09T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + $provider4->expects($this->once()) + ->method('send') + ->with($this->callback(function($vevent) { + if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-06-30T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + $provider5->expects($this->once()) + ->method('send') + ->with($this->callback(function($vevent) { + if ($vevent->DTSTART->getDateTime()->format(\DateTime::ATOM) !== '2016-07-07T00:00:00+00:00') { + return false; + } + return true; + }, 'Displayname 123', $user)); + + $this->backend->expects($this->at(1)) + ->method('removeReminder') + ->with(1); + $this->backend->expects($this->at(2)) + ->method('removeReminder') + ->with(2); + $this->backend->expects($this->at(3)) + ->method('removeReminder') + ->with(3); + $this->backend->expects($this->at(4)) + ->method('removeReminder') + ->with(4); + $this->backend->expects($this->at(5)) + ->method('insertReminder') + ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848700, false) + ->willReturn(99); + + $this->backend->expects($this->at(6)) + ->method('insertReminder') + ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848820, true) + ->willReturn(99); + $this->backend->expects($this->at(7)) + ->method('insertReminder') + ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467848940, true) + ->willReturn(99); + $this->backend->expects($this->at(8)) + ->method('insertReminder') + ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849060, true) + ->willReturn(99); + $this->backend->expects($this->at(9)) + ->method('insertReminder') + ->with(1337, 42, 'wej2z68l9h', true, 1467849600, false, 'fbdb2726bc0f7dfacac1d881c1453e20', 'ecacbf07d413c3c78d1ac7ad8c469602', 'EMAIL', true, 1467849180, true) + ->willReturn(99); + $this->backend->expects($this->at(10)) + ->method('removeReminder') + ->with(5); + $this->backend->expects($this->at(11)) + ->method('insertReminder') + ->with(1337, 42, 'wej2z68l9h', true, 1468454400, false, 'fbdb2726bc0f7dfacac1d881c1453e20', '8996992118817f9f311ac5cc56d1cc97', 'EMAIL', true, 1467763200, false) + ->willReturn(99); + + $this->timeFactory->method('getDateTime') + ->willReturn(\DateTime::createFromFormat(\DateTime::ATOM, '2016-06-08T00:00:00+00:00')); + + $this->reminderService->processReminders(); + } +}