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);