diff --git a/apps/dav/appinfo/database.xml b/apps/dav/appinfo/database.xml index b3a69de070c0a..84cc64b1623e0 100644 --- a/apps/dav/appinfo/database.xml +++ b/apps/dav/appinfo/database.xml @@ -798,4 +798,42 @@ CREATE TABLE calendarobjects ( + + *dbprefix*calendar_reminders + + + id + integer + 0 + true + 1 + true + 11 + + + user + text + 64 + + + calendarid + integer + 11 + + + objecturi + string + 255 + + + type + string + 255 + + + notificationDate + timestamp + + +
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 8be603ee9305a..d0e8268b974b9 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -34,6 +34,10 @@ OCA\DAV\Command\SyncSystemAddressBook + + OCA\DAV\CalDAV\Reminder\ReminderJob + + OCA\DAV\CalDAV\Activity\Filter\Calendar diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 5d89324d4a9cf..0983c03d52ce8 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -28,6 +28,8 @@ use OCA\DAV\CalDAV\Activity\Backend; use OCA\DAV\CalDAV\Activity\Provider\Event; use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend; +use OCA\DAV\CalDAV\Reminder\Notifier; use OCA\DAV\Capabilities; use OCA\DAV\CardDAV\ContactsManager; use OCA\DAV\CardDAV\PhotoCache; @@ -40,6 +42,8 @@ class Application extends App { + const APP_ID = 'dav'; + /** * Application constructor. */ @@ -86,8 +90,7 @@ public function registerHooks() { } }); - // carddav/caldav sync event setup - $listener = function($event) { + $birthdayListener = function ($event) { if ($event instanceof GenericEvent) { /** @var BirthdayService $b */ $b = $this->getContainer()->query(BirthdayService::class); @@ -99,9 +102,9 @@ public function registerHooks() { } }; - $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); @@ -182,6 +185,16 @@ public function registerHooks() { $event->getArgument('shares'), $event->getArgument('objectData') ); + + /** @var ReminderBackend $reminderBackend */ + $reminderBackend = $this->getContainer()->query(ReminderBackend::class); + + $reminderBackend->onTouchCalendarObject( + $eventName, + $event->getArgument('calendarData'), + $event->getArgument('shares'), + $event->getArgument('objectData') + ); }; $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', $listener); $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', $listener); @@ -192,4 +205,16 @@ public function getSyncService() { return $this->getContainer()->query(SyncService::class); } + public function registerNotifier() { + $this->getContainer()->getServer()->getNotificationManager()->registerNotifier(function() { + return $this->getContainer()->query(Notifier::class); + }, function() { + $l = $this->getContainer()->getServer()->getL10NFactory()->get(self::APP_ID); + return [ + 'id' => self::APP_ID, + 'name' => $l->t('Calendars and Contacts'), + ]; + }); + } + } diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 7fe18cd8656ee..27cebd1119eb5 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -31,6 +31,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder; use OCA\DAV\Connector\Sabre\Principal; use OCA\DAV\DAV\Sharing\Backend; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUser; use OCP\IUserManager; @@ -157,6 +158,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param IDBConnection $db * @param Principal $principalBackend * @param IUserManager $userManager + * @param IConfig $config * @param ISecureRandom $random * @param EventDispatcherInterface $dispatcher * @param bool $legacyEndpoint @@ -164,6 +166,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function __construct(IDBConnection $db, Principal $principalBackend, IUserManager $userManager, + IConfig $config, ISecureRandom $random, EventDispatcherInterface $dispatcher, $legacyEndpoint = false) { @@ -1001,7 +1004,6 @@ function createCalendarObject($calendarId, $objectUri, $calendarData) { */ function updateCalendarObject($calendarId, $objectUri, $calendarData) { $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 0000000000000..f6f02b9de9fe7 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/Backend.php @@ -0,0 +1,254 @@ + + * + * @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 OCA\DAV\CalDAV\CalDavBackend; +use OCP\IDBConnection; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUserSession; +use Sabre\VObject; +use Sabre\VObject\Component\VAlarm; +use Sabre\VObject\Reader; + +/** + * Class Backend + * + * @package OCA\DAV\CalDAV\Reminder + */ +class Backend { + + /** @var IGroupManager */ + protected $groupManager; + + /** @var IUserSession */ + protected $userSession; + + /** @var IDBConnection */ + protected $db; + + /** @var CalDavBackend */ + protected $calDavBackend; + + const ALARM_TYPES = ['AUDIO', 'EMAIL', 'DISPLAY']; + + /** + * @param IDBConnection $db + * @param CalDavBackend $calDavBackend + * @param IGroupManager $groupManager + * @param IUserSession $userSession + */ + public function __construct(IDBConnection $db, CalDavBackend $calDavBackend, IGroupManager $groupManager, IUserSession $userSession) { + $this->db = $db; + $this->calDavBackend = $calDavBackend; + $this->groupManager = $groupManager; + $this->userSession = $userSession; + } + + /** + * Saves reminders when a calendar object with some alarms was created/updated/deleted + * + * @param string $action + * @param array $calendarData + * @param array $shares + * @param array $objectData + */ + public function onTouchCalendarObject($action, array $calendarData, array $shares, array $objectData) { + if (!isset($calendarData['principaluri'])) { + return; + } + + if ($action === '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject') { + $this->cleanRemindersForEvent($calendarData['id'], $objectData['uri']); + return; + } + + $principal = explode('/', $calendarData['principaluri']); + $owner = array_pop($principal); + + $object = $this->getObjectNameAndType($objectData); + + $users = $this->getUsersForShares($shares); + $users[] = $owner; + + $this->cleanRemindersForEvent($objectData['calendarid'], $objectData['uri']); + + $vobject = VObject\Reader::read($objectData['calendardata']); + + foreach ($vobject->VEVENT->VALARM as $alarm) { + if ($alarm instanceof VAlarm) { + $type = strtoupper($alarm->ACTION->getValue()); + if (in_array($type, self::ALARM_TYPES, true)) { + $time = $alarm->getEffectiveTriggerTime(); + + foreach ($users as $user) { + $query = $this->db->getQueryBuilder(); + $query->insert('calendar_reminders') + ->values([ + 'user' => $query->createNamedParameter($user), + 'calendarid' => $query->createNamedParameter($objectData['calendarid']), + 'objecturi' => $query->createNamedParameter($objectData['uri']), + 'type' => $query->createNamedParameter($type), + 'notificationDate' => $query->createNamedParameter($time->getTimestamp()), + ])->execute(); + } + } + } + } + } + + /** + * @param array $objectData + * @return string[]|bool + */ + protected function getObjectNameAndType(array $objectData) { + $vObject = Reader::read($objectData['calendardata']); + $component = $componentType = null; + foreach($vObject->getComponents() as $component) { + if (in_array($component->name, ['VEVENT', 'VTODO'], true)) { + $componentType = $component->name; + break; + } + } + + if (!$componentType) { + // Calendar objects must have a VEVENT or VTODO component + return false; + } + + if ($componentType === 'VEVENT') { + return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'event']; + } + return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'todo', 'status' => (string) $component->STATUS]; + } + + /** + * Get all users that have access to a given calendar + * + * @param array $shares + * @return string[] + */ + protected function getUsersForShares(array $shares) + { + $users = $groups = []; + foreach ($shares as $share) { + $prinical = explode('/', $share['{http://owncloud.org/ns}principal']); + if ($prinical[1] === 'users') { + $users[] = $prinical[2]; + } else if ($prinical[1] === 'groups') { + $groups[] = $prinical[2]; + } + } + + if (!empty($groups)) { + foreach ($groups as $gid) { + $group = $this->groupManager->get($gid); + if ($group instanceof IGroup) { + foreach ($group->getUsers() as $user) { + $users[] = $user->getUID(); + } + } + } + } + + return array_unique($users); + } + + /** + * Cleans reminders in database + * + * @param string $calendarId + * @param string $objectUri + */ + public function cleanRemindersForEvent($calendarId, $objectUri) + { + $query = $this->db->getQueryBuilder(); + + $query->delete('calendar_reminders') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->andWhere($query->expr()->eq('objecturi', $query->createNamedParameter($objectUri))) + ->execute(); + } + + public function cleanRemindersForCalendar($calendarId) + { + $query = $this->db->getQueryBuilder(); + + $query->delete('calendar_reminders') + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->execute(); + } + + public function removeReminder($reminderId) + { + $query = $this->db->getQueryBuilder(); + + $query->delete('calendar_reminders') + ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) + ->execute(); + } + + /** + * Get reminders + * + * @return array + */ + public function getReminders() + { + $query = $this->db->getQueryBuilder(); + $fields = ['id', 'calendarid', 'objecturi', 'type', 'notificationDate', 'user']; + $result = $query->select($fields) + ->from('calendar_reminders') + ->execute(); + + $reminders = []; + while($row = $result->fetch(\PDO::FETCH_ASSOC)) { + $reminder = [ + 'id' => $row['id'], + 'user' => $row['user'], + 'calendarId' => $row['calendarid'], + 'objecturi' => $row['objecturi'], + 'type' => $row['type'], + 'notificationDate' => $row['notificationDate'] + ]; + + $reminder['event'] = $this->getCalendarObject($reminder['calendarId'], $reminder['objecturi']); + + $reminder['calendar'] = $this->getCalendarById($reminder['calendarId']); + + $reminders[] = $reminder; + + } + return $reminders; + } + + public function getCalendarById($id) + { + return $this->calDavBackend->getCalendarById($id); + } + + public function getCalendarObject($calendarId, $objectUri) + { + return $this->calDavBackend->getCalendarObject($calendarId, $objectUri); + } +} diff --git a/apps/dav/lib/CalDAV/Reminder/Notifier.php b/apps/dav/lib/CalDAV/Reminder/Notifier.php new file mode 100644 index 0000000000000..e939d179f79be --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/Notifier.php @@ -0,0 +1,58 @@ + + * + * @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 OCP\L10N\IFactory; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; + +class Notifier implements INotifier { + protected $factory; + + public function __construct(IFactory $factory) { + $this->factory = $factory; + } + + /** + * @param INotification $notification + * @param string $languageCode The code of the language that should be used to prepare the notification + * @return INotification + */ + public function prepare(INotification $notification, $languageCode) { + if ($notification->getApp() !== 'dav') { + throw new \InvalidArgumentException(); + } + + // Read the language from the notification + $l = $this->factory->get('dav', $languageCode); + + if ($notification->getSubject() === 'calendar_reminder') { + $subjectParams = $notification->getSubjectParameters(); + $notification->setParsedSubject((string)$l->t('Your event "%s" is in %s', [$subjectParams[0], date_format($subjectParams[1], 'Y-m-d H:i:s')])); + $notification->setParsedMessage($notification->getMessageParameters()[0]); + } else { + // Unknown subject => Unknown notification => throw + throw new \InvalidArgumentException(); + } + return $notification; + } +} \ No newline at end of file diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderJob.php b/apps/dav/lib/CalDAV/Reminder/ReminderJob.php new file mode 100644 index 0000000000000..87bf24b7d4445 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/ReminderJob.php @@ -0,0 +1,187 @@ + + * + * @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 OC\BackgroundJob\TimedJob; +use OCP\IL10N; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Mail\IMailer; +use OCP\Defaults; +use OCP\IConfig; +use OCP\Notification\IManager; +use OCP\Notification\INotification; +use OCP\Util; +use Sabre\VObject\Component; +use Sabre\VObject\Reader; + +class ReminderJob extends TimedJob { + + /** @var IConfig */ + private $config; + + /** @var Defaults */ + private $defaults; + + /** @var IMailer */ + private $mailer; + + /** @var IL10N */ + private $l10n; + + /** @var IManager */ + private $notifications; + + /** @var Backend */ + private $backend; + + /** @var IUserManager */ + private $usermanager; + + public function __construct(IConfig $config, Defaults $defaults, IMailer $mailer, IL10N $l10n, IManager $notifications, Backend $backend, IUserManager $usermanager) { + $this->config = $config; + $this->defaults = $defaults; + $this->mailer = $mailer; + $this->l10n = $l10n; + $this->notifications = $notifications; + $this->backend = $backend; + $this->usermanager = $usermanager; + + /** Run every 15 minutes */ + $this->setInterval(10); + } + + /** + * @param $arg + */ + public function run($arg) { + $reminders = $this->backend->getReminders(); + + foreach ($reminders as $reminder) { + if ($reminder['notificationDate'] < new \DateTime()) { + $reminder['eventData'] = Reader::read($reminder['event']['calendardata']); + + $reminderDetails = $this->getDetails($reminder); + + if ($reminder['type'] === 'EMAIL') { + $this->sendMail($this->usermanager->get($reminder['user']), $reminderDetails); + } elseif ($reminder['type'] === 'DISPLAY') { + $this->sendNotification($this->usermanager->get($reminder['user']), $reminderDetails); + } + $this->backend->removeReminder($reminder['id']); + } + } + } + + private function getDetails(array $reminder) + { + $component = null; + + /** + * Get the real event + */ + foreach($reminder['eventData']->getComponents() as $component) { + /** @var Component $component */ + if ($component->name === 'VEVENT') { + break; + } + } + + /** + * Try to get geocoordinates + */ + $geo = null; + if (isset($component->GEO)) { + list($geo['lat'], $geo['long']) = explode(';', $component->GEO, 2); + } + + /** + * Build the list of attendees + */ + + + return [ + 'title' => $component->SUMMARY, + 'start' => $component->DTSTART->getDateTime(), + 'location' => $component->LOCATION, + 'geo' => $geo, + 'description' => $component->DESCRIPTION, + 'calendarName' => $reminder['calendar']['{DAV:}displayname'], + 'participants' => $component->ATTENDEE, + 'notificationDate' => $reminder['notificationDate'], + 'uri' => $reminder['objecturi'], + ]; + } + + private function sendMail(IUser $user, array $details) { + + $message = $this->mailer->createMessage(); + $template = $this->mailer->createEMailTemplate(); + + $template->addHeader(); + $template->addHeading($this->l10n->t('Notification: %s - ', [$details['title']]) . $this->l10n->l('datetime', $details['start'])); + + $template->addBodyText($this->l10n->t('Hello,')); + + $template->addBodyText($details['title']); + + if ($details['location']) { + if ($details['geo']) { + // if we have exact coordinates, put a link to OSM on the location string + $template->addBodyButton($this->l10n->t('Where: %s', [$details['location']]), 'https://www.openstreetmap.org/#map=16/' . $details['geo']['lat'] . '/' . $details['geo']['long']); + } else { + // if we have a location field, show it + $template->addBodyText($this->l10n->t('Where: %s', [$details['location']])); + } + } + + $template->addBodyText($this->l10n->t('Calendar: %s', [$details['calendarName']])); + + if ($details['participants']) { + $template->addBodyText($this->l10n->t('Attendees: %s', [implode(', ', $details['participants'])])); + } + + $body = $template->renderHtml(); + $plainBody = $template->renderText(); + + $from = Util::getDefaultEmailAddress('register'); + + $message->setFrom([$from => $this->defaults->getName()]); + $message->setTo([$user->getEMailAddress() => 'Recipient']); + $message->setPlainBody($plainBody); + $message->setHtmlBody($body); + + $this->mailer->send($message); + } + + private function sendNotification(IUser $user, $reminder) { + /** @var INotification $notification */ + $notification = $this->notifications->createNotification(); + $notification->setApp('dav') + ->setUser($user->getUID()) + //->setDateTime(\DateTime::createFromFormat('U', $reminder['notificationDate'])) + ->setDateTime(new \DateTime()) + ->setObject('calendar_reminder', $reminder['uri']) // $type and $id + ->setSubject('calendar_reminder', [$reminder['title'], $reminder['start']]) // $subject and $parameters + ->setMessage('calendar_reminder', ['hurry up !']) + ; + $this->notifications->notify($notification); + } +} \ No newline at end of file diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index a243ec6d00a1a..a5ecb95080cf5 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -62,7 +62,7 @@ public function __construct() { $systemPrincipals->disableListing = $disableListing; $filesCollection = new Files\RootCollection($userPrincipalBackend, 'principals/users'); $filesCollection->disableListing = $disableListing; - $caldavBackend = new CalDavBackend($db, $userPrincipalBackend, $userManager, $random, $dispatcher); + $caldavBackend = new CalDavBackend($db, $userPrincipalBackend, $userManager, $config, $random, $dispatcher); $calendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users'); $calendarRoot->disableListing = $disableListing; $publicCalendarRoot = new PublicCalendarRoot($caldavBackend);