diff options
author | Georg Ehrke <developer@georgehrke.com> | 2019-08-09 20:25:21 +0200 |
---|---|---|
committer | Roeland Jago Douma <roeland@famdouma.nl> | 2019-08-15 20:03:51 +0200 |
commit | bcce568c6d892ccacc598c5d5bfa6b24c3284741 (patch) | |
tree | 5ecf26f407ac71ef7b97e99a9f3f026c3ec6974e /apps/dav | |
parent | 7c4a8a3bdf6d8c0c12cf7cab93cc465c83c4ec8f (diff) | |
download | nextcloud-server-bcce568c6d892ccacc598c5d5bfa6b24c3284741.tar.gz nextcloud-server-bcce568c6d892ccacc598c5d5bfa6b24c3284741.zip |
Support recurring events + repeating alarms
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
Diffstat (limited to 'apps/dav')
20 files changed, 1908 insertions, 576 deletions
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 6e0219e91dd..81b93de055d 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ <name>WebDAV</name> <summary>WebDAV endpoint</summary> <description>WebDAV endpoint</description> - <version>1.11.1</version> + <version>1.13.0</version> <licence>agpl</licence> <author>owncloud.org</author> <namespace>DAV</namespace> diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index bd26290cc9a..a1c2d671b8a 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -52,9 +52,10 @@ 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\\AbstractNotificationProvider' => $baseDir . '/../lib/CalDAV/Reminder/AbstractNotificationProvider.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', @@ -188,7 +189,6 @@ return array( 'OCA\\DAV\\Migration\\Version1005Date20180530124431' => $baseDir . '/../lib/Migration/Version1005Date20180530124431.php', 'OCA\\DAV\\Migration\\Version1006Date20180619154313' => $baseDir . '/../lib/Migration/Version1006Date20180619154313.php', 'OCA\\DAV\\Migration\\Version1006Date20180628111625' => $baseDir . '/../lib/Migration/Version1006Date20180628111625.php', - 'OCA\\DAV\\Migration\\Version1007Date20181005133326' => $baseDir . '/../lib/Migration/Version1007Date20181005133326.php', 'OCA\\DAV\\Migration\\Version1008Date20181030113700' => $baseDir . '/../lib/Migration/Version1008Date20181030113700.php', 'OCA\\DAV\\Migration\\Version1008Date20181105104826' => $baseDir . '/../lib/Migration/Version1008Date20181105104826.php', 'OCA\\DAV\\Migration\\Version1008Date20181105104833' => $baseDir . '/../lib/Migration/Version1008Date20181105104833.php', @@ -197,6 +197,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 1a0ce9d02f0..39488419f87 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -67,9 +67,10 @@ 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\\AbstractNotificationProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Reminder/AbstractNotificationProvider.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', @@ -203,7 +204,6 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1005Date20180530124431' => __DIR__ . '/..' . '/../lib/Migration/Version1005Date20180530124431.php', 'OCA\\DAV\\Migration\\Version1006Date20180619154313' => __DIR__ . '/..' . '/../lib/Migration/Version1006Date20180619154313.php', 'OCA\\DAV\\Migration\\Version1006Date20180628111625' => __DIR__ . '/..' . '/../lib/Migration/Version1006Date20180628111625.php', - 'OCA\\DAV\\Migration\\Version1007Date20181005133326' => __DIR__ . '/..' . '/../lib/Migration/Version1007Date20181005133326.php', 'OCA\\DAV\\Migration\\Version1008Date20181030113700' => __DIR__ . '/..' . '/../lib/Migration/Version1008Date20181030113700.php', 'OCA\\DAV\\Migration\\Version1008Date20181105104826' => __DIR__ . '/..' . '/../lib/Migration/Version1008Date20181105104826.php', 'OCA\\DAV\\Migration\\Version1008Date20181105104833' => __DIR__ . '/..' . '/../lib/Migration/Version1008Date20181105104833.php', @@ -212,6 +212,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/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 5d17dc5a241..80e9dea8829 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -231,12 +231,10 @@ class Application extends App { ); /** @var ReminderService $reminderBackend */ - $reminderService= $this->getContainer()->query(ReminderService::class); + $reminderService = $this->getContainer()->query(ReminderService::class); $reminderService->onTouchCalendarObject( $eventName, - $event->getArgument('calendarData'), - $event->getArgument('shares'), $event->getArgument('objectData') ); }; diff --git a/apps/dav/lib/CalDAV/Reminder/AbstractNotificationProvider.php b/apps/dav/lib/CalDAV/Reminder/AbstractNotificationProvider.php deleted file mode 100644 index 1d858627de9..00000000000 --- a/apps/dav/lib/CalDAV/Reminder/AbstractNotificationProvider.php +++ /dev/null @@ -1,205 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr> - * - * @author Thomas Citharel <tcit@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -namespace OCA\DAV\CalDAV\Reminder; - -use \DateTime; -use \DateTimeImmutable; -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\VCalendar; -use Sabre\VObject\Component\VEvent; -use Sabre\VObject\DateTimeParser; -use Sabre\VObject\Parameter; -use Sabre\VObject\Property; - -abstract class AbstractNotificationProvider { - - /** @var string */ - public const NOTIFICATION_TYPE = ''; - - /** @var ILogger */ - protected $logger; - - /** @var L10NFactory */ - protected $l10nFactory; - - /** @var IL10N */ - protected $l10n; - - /** @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 VCalendar $vcalendar - * @param string $calendarDisplayName - * @param IUser $user - * @return void - */ - abstract function send(VCalendar $vcalendar, string $calendarDisplayName, IUser $user): void; - - /** - * @var VCalendar $vcalendar - * @var string $defaultValue - * @return array - * @throws \Exception - */ - protected function extractEventDetails(VCalendar $vcalendar, $defaultValue = ''):array { - /** @var VEvent $vevent */ - $vevent = $vcalendar->VEVENT; - - /** @var Property $start */ - $start = $vevent->DTSTART; - if (isset($vevent->DTEND)) { - $end = $vevent->DTEND; - } elseif (isset($vevent->DURATION)) { - $isFloating = $vevent->DTSTART->isFloating(); - $end = clone $vevent->DTSTART; - $endDateTime = $end->getDateTime(); - $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue())); - $end->setDateTime($endDateTime, $isFloating); - } elseif (!$vevent->DTSTART->hasTime()) { - $isFloating = $vevent->DTSTART->isFloating(); - $end = clone $vevent->DTSTART; - $endDateTime = $end->getDateTime(); - $endDateTime = $endDateTime->modify('+1 day'); - $end->setDateTime($endDateTime, $isFloating); - } else { - $end = clone $vevent->DTSTART; - } - - return [ - 'title' => (string) $vevent->SUMMARY ?: $defaultValue, - 'description' => (string) $vevent->DESCRIPTION ?: $defaultValue, - 'start'=> $start->getDateTime(), - 'end' => $end->getDateTime(), - 'when' => $this->generateWhenString($start, $end), - 'url' => (string) $vevent->URL ?: $defaultValue, - 'location' => (string) $vevent->LOCATION ?: $defaultValue, - 'uid' => (string) $vevent->UID, - ]; - } - - /** - * @param Property $dtstart - * @param Property $dtend - * @return string - * @throws \Exception - */ - private function generateWhenString(Property $dtstart, Property $dtend):string { - $isAllDay = $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 = $dtstart->getDateTime(); - /** @var DateTimeImmutable $dtendDt */ - $dtendDt = $dtend->getDateTime(); - - $diff = $dtstartDt->diff($dtendDt); - - $dtstartDt = new DateTime($dtstartDt->format(DateTime::ATOM)); - $dtendDt = new DateTime($dtendDt->format(DateTime::ATOM)); - - if ($isAllDay) { - // One day event - if ($diff->days === 1) { - return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']); - } - - //event that spans over multiple days - $localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']); - $localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']); - - return $localeStart . ' - ' . $localeEnd; - } - - /** @var Property\ICalendar\DateTime $dtstart */ - /** @var Property\ICalendar\DateTime $dtend */ - $isFloating = $dtstart->isFloating(); - $startTimezone = $endTimezone = null; - if (!$isFloating) { - $prop = $dtstart->offsetGet('TZID'); - if ($prop instanceof Parameter) { - $startTimezone = $prop->getValue(); - } - - $prop = $dtend->offsetGet('TZID'); - if ($prop instanceof Parameter) { - $endTimezone = $prop->getValue(); - } - } - - $localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' . - $this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']); - - // always show full date with timezone if timezones are different - if ($startTimezone !== $endTimezone) { - $localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']); - - return $localeStart . ' (' . $startTimezone . ') - ' . - $localeEnd . ' (' . $endTimezone . ')'; - } - - // show only end time if date is the same - if ($this->isDayEqual($dtstartDt, $dtendDt)) { - $localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']); - } else { - $localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' . - $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']); - } - - 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'); - } -} diff --git a/apps/dav/lib/CalDAV/Reminder/Backend.php b/apps/dav/lib/CalDAV/Reminder/Backend.php index 087a5785f36..be65c35da0f 100644 --- a/apps/dav/lib/CalDAV/Reminder/Backend.php +++ b/apps/dav/lib/CalDAV/Reminder/Backend.php @@ -1,8 +1,11 @@ <?php +declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Thomas Citharel <tcit@tcit.fr> + * @copyright Copyright (c) 2019, Thomas Citharel + * @copyright Copyright (c) 2019, Georg Ehrke * * @author Thomas Citharel <tcit@tcit.fr> + * @author Georg Ehrke <oc.list@georgehrke.com> * * @license GNU AGPL version 3 or any later version * @@ -20,7 +23,6 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ - namespace OCA\DAV\CalDAV\Reminder; use OCP\IDBConnection; @@ -40,95 +42,178 @@ class Backend { private $timeFactory; /** + * Backend constructor. + * * @param IDBConnection $db * @param ITimeFactory $timeFactory */ - public function __construct(IDBConnection $db, 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 string $calendarId - * @param string $uri + * @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 int $eventStartDate + * @param bool $isRepeatBased + * @return int The insert id */ - public function insertReminder(string $uid, string $calendarId, string $uri, string $type, int $notificationDate, int $eventStartDate):void { + 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), - 'calendarid' => $query->createNamedParameter($calendarId), - 'objecturi' => $query->createNamedParameter($uri), + '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), - 'notificationdate' => $query->createNamedParameter($notificationDate), - 'eventstartdate' => $query->createNamedParameter($eventStartDate), - ])->execute(); + 'is_relative' => $query->createNamedParameter($isRelative ? 1 : 0), + 'notification_date' => $query->createNamedParameter($notificationDate), + 'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0), + ]) + ->execute(); + + return $query->getLastInsertId(); } /** - * Cleans reminders in database + * Sets a new notificationDate on an existing reminder * - * @param int $calendarId - * @param string $objectUri + * @param int $reminderId + * @param int $newNotificationDate */ - public function cleanRemindersForEvent(int $calendarId, string $objectUri):void { + public function updateReminder(int $reminderId, + int $newNotificationDate):void { $query = $this->db->getQueryBuilder(); - - $query->delete('calendar_reminders') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) - ->andWhere($query->expr()->eq('objecturi', $query->createNamedParameter($objectUri))) + $query->update('calendar_reminders') + ->set('notification_date', $query->createNamedParameter($newNotificationDate)) + ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) ->execute(); } /** - * Remove all reminders for a calendar + * Remove a reminder by it's id * - * @param integer $calendarId + * @param integer $reminderId * @return void */ - public function cleanRemindersForCalendar(int $calendarId):void { + public function removeReminder(int $reminderId):void { $query = $this->db->getQueryBuilder(); $query->delete('calendar_reminders') - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) + ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) ->execute(); } /** - * Remove a reminder by it's id + * Cleans reminders in database * - * @param integer $reminderId - * @return void + * @param int $objectId */ - public function removeReminder(int $reminderId):void { + public function cleanRemindersForEvent(int $objectId):void { $query = $this->db->getQueryBuilder(); $query->delete('calendar_reminders') - ->where($query->expr()->eq('id', $query->createNamedParameter($reminderId))) + ->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId))) ->execute(); } /** - * Get all reminders with a notification date before now + * Remove all reminders for a calendar * - * @return array - * @throws \Exception + * @param int $calendarId + * @return void */ - public function getRemindersToProcess():array { + public function cleanRemindersForCalendar(int $calendarId):void { $query = $this->db->getQueryBuilder(); - $fields = ['cr.id', 'cr.calendarid', 'cr.objecturi', 'cr.type', 'cr.notificationdate', 'cr.uid', 'co.calendardata', 'c.displayname']; - $stmt = $query->select($fields) - ->from('calendar_reminders', 'cr') - ->where($query->expr()->lte('cr.notificationdate', $query->createNamedParameter($this->timeFactory->getTime()))) - ->andWhere($query->expr()->gte('cr.eventstartdate', $query->createNamedParameter($this->timeFactory->getTime()))) # We check that DTSTART isn't before - ->leftJoin('cr', 'calendars', 'c', $query->expr()->eq('cr.calendarid', 'c.id')) - ->leftJoin('cr', 'calendarobjects', 'co', $query->expr()->andX($query->expr()->eq('cr.calendarid', 'c.id'), $query->expr()->eq('co.uri', 'cr.objecturi'))) + + $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 $stmt->fetchAll(); + 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 00000000000..d0e526eb0ee --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php @@ -0,0 +1,47 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\DAV\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 00000000000..6b2364c8022 --- /dev/null +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php @@ -0,0 +1,191 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019, Thomas Citharel + * @copyright Copyright (c) 2019, Georg Ehrke + * + * @author Thomas Citharel <tcit@tcit.fr> + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; + +use \DateTime; +use \DateTimeImmutable; +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\Parameter; +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 index 6e702bcacaa..ad4ac342f66 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php @@ -1,4 +1,5 @@ <?php +declare(strict_types=1); /** * @copyright Copyright (c) 2019, Georg Ehrke * diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php index f05439932b6..2a7eb2a4032 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php @@ -1,8 +1,11 @@ <?php +declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr> + * @copyright Copyright (c) 2019, Thomas Citharel + * @copyright Copyright (c) 2019, Georg Ehrke * * @author Thomas Citharel <tcit@tcit.fr> + * @author Georg Ehrke <oc.list@georgehrke.com> * * @license GNU AGPL version 3 or any later version * @@ -20,27 +23,36 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ - namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; -use OCA\DAV\CalDAV\Reminder\AbstractNotificationProvider; +use DateTime; +use DateTimeImmutable; 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\VCalendar; - -class EmailProvider extends AbstractNotificationProvider { +use Sabre\VObject\Component\VEvent; +use Sabre\VObject; +use Sabre\VObject\Parameter; +use Sabre\VObject\Property; - /** @var IMailer */ - private $mailer; +/** + * 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 @@ -48,7 +60,9 @@ class EmailProvider extends AbstractNotificationProvider { * @param L10NFactory $l10nFactory * @param IUrlGenerator $urlGenerator */ - public function __construct(IConfig $config, IMailer $mailer, ILogger $logger, + public function __construct(IConfig $config, + IMailer $mailer, + ILogger $logger, L10NFactory $l10nFactory, IURLGenerator $urlGenerator) { parent::__construct($logger, $l10nFactory, $urlGenerator, $config); @@ -56,90 +70,100 @@ class EmailProvider extends AbstractNotificationProvider { } /** - * Send notification + * Send out notification via email * - * @param VCalendar $vcalendar + * @param VEvent $vevent * @param string $calendarDisplayName - * @param IUser $user - * @return void + * @param array $users * @throws \Exception */ - public function send(VCalendar $vcalendar, string $calendarDisplayName, IUser $user):void { - if ($user->getEMailAddress() === null) { - return; - } + public function send(VEvent $vevent, + string $calendarDisplayName, + array $users=[]):void { + $fallbackLanguage = $this->getFallbackLanguage(); - $lang = $this->config->getUserValue($user->getUID(), 'core', 'lang', $this->l10nFactory->findLanguage()); - $this->l10n = $this->l10nFactory->get('dav', $lang); + $emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users); + $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent); - $event = $this->extractEventDetails($vcalendar); - $fromEMail = \OCP\Util::getDefaultEmailAddress('invitations-noreply'); + // 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 + ); - $message = $this->mailer->createMessage() - ->setFrom([$fromEMail => 'Nextcloud']) - // TODO: Set reply to from event creator - // ->setReplyTo([$sender => $senderName]) - ->setTo([$user->getEMailAddress() => $user->getDisplayName()]); + $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', $event); - $template->addHeader(); + $message = $this->mailer->createMessage(); + $message->setFrom([$fromEMail]); + if ($organizer) { + $message->setReplyTo($organizer); + } + $message->setBcc($emailAddresses); - $this->addSubjectAndHeading($template, $event['title']); - $this->addBulletList($template, $event, $calendarDisplayName); + $template = $this->mailer->createEMailTemplate('dav.calendarReminder'); + $template->addHeader(); - $template->addFooter(); - $message->useTemplate($template); + $this->addSubjectAndHeading($template, $l10n, $vevent); + $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent); - $attachment = $this->mailer->createAttachment( - $vcalendar->serialize(), - $event['uid'].'.ics',// TODO(leon): Make file name unique, e.g. add event id - 'text/calendar' - ); - $message->attach($attachment); + $template->addFooter(); + $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)]); + 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']); } - } catch(\Exception $ex) { - $this->logger->logException($ex, ['app' => 'dav']); } } /** * @param IEMailTemplate $template - * @param string $summary + * @param IL10N $l10n + * @param VEvent $vevent */ - private function addSubjectAndHeading(IEMailTemplate $template, string $summary):void { - $template->setSubject('Notification: ' . $summary); - $template->addHeading($summary); + 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 array $eventData + * @param IL10N $l10n * @param string $calendarDisplayName + * @param array $eventData */ - private function addBulletList(IEMailTemplate $template, array $eventData, string $calendarDisplayName):void { - $template->addBodyListItem($calendarDisplayName, $this->l10n->t('Calendar:'), + 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($eventData['when'], $this->l10n->t('Date:'), + $template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'), $this->getAbsoluteImagePath('places/calendar.svg')); - if ($eventData['location']) { - $template->addBodyListItem((string) $eventData['location'], $this->l10n->t('Where:'), + if (isset($vevent->LOCATION)) { + $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'), $this->getAbsoluteImagePath('actions/address.svg')); } - if ($eventData['description']) { - $template->addBodyListItem((string) $eventData['description'], $this->l10n->t('Description:'), + if (isset($vevent->DESCRIPTION)) { + $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'), $this->getAbsoluteImagePath('actions/more.svg')); } - if ($eventData['url']) { - $template->addBodyListItem((string) $eventData['url'], $this->l10n->t('Link:'), - $this->getAbsoluteImagePath('places/link.svg')); - } } /** @@ -151,4 +175,355 @@ class EmailProvider extends AbstractNotificationProvider { $this->urlGenerator->imagePath('core', $path) ); } + + /** + * @param VEvent $vevent + * @return array|null + */ + private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array { + if (!$vevent->ORGANIZER) { + return null; + } + + $organizer = $vevent->ORGANZIER; + 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 $sortedByLanguage + * @param IUser[] $users + * @param string $defaultLanguage + */ + private function sortUsersByLanguage(array &$sortedByLanguage, + array $users, + string $defaultLanguage):void { + /** + * @var array $sortedByLanguage + * [ + * 'de' => ['a@b.com', 'c@d.com'], + * ... + * ] + */ + foreach($users as $user) { + /** @var IUser $user */ + $emailAddress = $user->getEMailAddress(); + $lang = $this->config->getUserValue($user->getUID(), + 'core', 'lang', $defaultLanguage); + + if (!isset($sortedByLanguage[$lang])) { + $sortedByLanguage[$lang] = []; + } + + $sortedByLanguage[$lang][] = $emailAddress; + } + } + + /** + * @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)) { + $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 strcasecmp($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 array_unique($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); + + $dtstartDt = new \DateTime($dtstartDt->format(\DateTime::ATOM)); + $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()) { + $startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName(); + $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 index bf736db8a34..bfa6db95852 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php @@ -1,4 +1,5 @@ <?php +declare(strict_types=1); /** * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr> * @@ -20,7 +21,6 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ - namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; class ProviderNotAvailableException extends \Exception { diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php index f04b8e4c45a..2e580fd78a3 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php @@ -1,8 +1,11 @@ <?php +declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr> + * @copyright Copyright (c) 2019, Thomas Citharel + * @copyright Copyright (c) 2019, Georg Ehrke * * @author Thomas Citharel <tcit@tcit.fr> + * @author Georg Ehrke <oc.list@georgehrke.com> * * @license GNU AGPL version 3 or any later version * @@ -20,11 +23,9 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ - namespace OCA\DAV\CalDAV\Reminder\NotificationProvider; use OCA\DAV\AppInfo\Application; -use OCA\DAV\CalDAV\Reminder\AbstractNotificationProvider; use OCP\IConfig; use OCP\ILogger; use OCP\IURLGenerator; @@ -32,22 +33,24 @@ use OCP\L10N\IFactory as L10NFactory; use OCP\Notification\IManager; use OCP\IUser; use OCP\Notification\INotification; -use Sabre\VObject\Component\VCalendar; use OCP\AppFramework\Utility\ITimeFactory; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Property; -class PushProvider extends AbstractNotificationProvider { +/** + * Class PushProvider + * + * @package OCA\DAV\CalDAV\Reminder\NotificationProvider + */ +class PushProvider extends AbstractProvider { /** @var string */ public const NOTIFICATION_TYPE = 'DISPLAY'; - /** - * @var IManager - */ + /** @var IManager */ private $manager; - /** - * @var ITimeFactory - */ + /** @var ITimeFactory */ private $timeFactory; /** @@ -58,42 +61,75 @@ class PushProvider extends AbstractNotificationProvider { * @param IUrlGenerator $urlGenerator * @param ITimeFactory $timeFactory */ - public function __construct(IConfig $config, IManager $manager, ILogger $logger, + public function __construct(IConfig $config, + IManager $manager, + ILogger $logger, L10NFactory $l10nFactory, - IURLGenerator $urlGenerator, ITimeFactory $timeFactory) { + IURLGenerator $urlGenerator, + ITimeFactory $timeFactory) { parent::__construct($logger, $l10nFactory, $urlGenerator, $config); $this->manager = $manager; $this->timeFactory = $timeFactory; } /** - * Send notification + * Send push notification to all users. * - * @param VCalendar $vcalendar + * @param VEvent $vevent * @param string $calendarDisplayName - * @param IUser $user - * @return void + * @param IUser[] $users * @throws \Exception */ - public function send(VCalendar $vcalendar, string $calendarDisplayName, IUser $user):void { - $lang = $this->config->getUserValue($user->getUID(), 'core', 'lang', $this->l10nFactory->findLanguage()); - $this->l10n = $this->l10nFactory->get('dav', $lang); + public function send(VEvent $vevent, + string $calendarDisplayName=null, + array $users=[]):void { + $eventDetails = $this->extractEventDetails($vevent); + $eventDetails['calendar_displayname'] = $calendarDisplayName; - $event = $this->extractEventDetails($vcalendar); - /** @var INotification $notification */ - $notification = $this->manager->createNotification(); - $notification->setApp(Application::APP_ID) - ->setUser($user->getUID()) - ->setDateTime($this->timeFactory->getDateTime()) - ->setObject(Application::APP_ID, $event['uid']) // $type and $id - ->setSubject('calendar_reminder', ['title' => $event['title'], 'start' => $event['start']->getTimestamp()]) // $subject and $parameters - ->setMessage('calendar_reminder', [ - 'when' => $event['when'], - 'description' => $event['description'], - 'location' => $event['location'], - 'calendar' => $calendarDisplayName - ]) - ; - $this->manager->notify($notification); + 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, + 'start_atom' => $start->getDateTime()->format(\DateTime::ATOM), + 'start_is_floating' => $start->isFloating(), + 'start_timezone' => $start->getDateTime()->getTimezone()->getName(), + 'end_atom' => $end->getDateTime()->format(\DateTime::ATOM), + 'end_is_floating' => $end->isFloating(), + 'end_timezone' => $end->getDateTime()->getTimezone()->getName(), + ]; + } } diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php index 0a2579aedfb..3d54970562d 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php @@ -1,6 +1,11 @@ <?php +declare(strict_types=1); /** + * @copyright Copyright (c) 2019, Thomas Citharel + * @copyright Copyright (c) 2019, Georg Ehrke + * * @author Thomas Citharel <tcit@tcit.fr> + * @author Georg Ehrke <oc.list@georgehrke.com> * * @license AGPL-3.0 * @@ -19,36 +24,55 @@ */ namespace OCA\DAV\CalDAV\Reminder; -use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException; - +/** + * Class NotificationProviderManager + * + * @package OCA\DAV\CalDAV\Reminder + */ class NotificationProviderManager { - /** @var array */ + /** @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])); + } + /** - * @var string $type - * @return AbstractNotificationProvider - * @throws ProviderNotAvailableException + * Get provider for a given ACTION + * + * @param string $type + * @return INotificationProvider + * @throws NotificationProvider\ProviderNotAvailableException * @throws NotificationTypeDoesNotExistException */ - public function getProvider(string $type):AbstractNotificationProvider { + 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 ProviderNotAvailableException($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 AbstractNotificationProvider) { + if (!$provider instanceof INotificationProvider) { throw new \InvalidArgumentException('Invalid notification provider registered'); } diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php index ae4ec3bd3b7..c060089785a 100644 --- a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php +++ b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php @@ -1,6 +1,7 @@ <?php +declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr> + * @copyright Copyright (c) 2019, Thomas Citharel * * @author Thomas Citharel <tcit@tcit.fr> * diff --git a/apps/dav/lib/CalDAV/Reminder/Notifier.php b/apps/dav/lib/CalDAV/Reminder/Notifier.php index 3718d5b29a6..4bad9841787 100644 --- a/apps/dav/lib/CalDAV/Reminder/Notifier.php +++ b/apps/dav/lib/CalDAV/Reminder/Notifier.php @@ -1,8 +1,11 @@ <?php +declare(strict_types=1); /** - * @copyright Copyright (c) 2019 Thomas Citharel <tcit@tcit.fr> + * @copyright Copyright (c) 2019, Thomas Citharel + * @copyright Copyright (c) 2019, Georg Ehrke * * @author Thomas Citharel <tcit@tcit.fr> + * @author Georg Ehrke <oc.list@georgehrke.com> * * @license AGPL-3.0 * @@ -22,37 +25,47 @@ 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 array */ - public static $units = [ - 'y' => 'year', - 'm' => 'month', - 'd' => 'day', - 'h' => 'hour', - 'i' => 'minute', - 's' => 'second', - ]; - /** @var IFactory */ - protected $factory; + private $l10nFactory; /** @var IURLGenerator */ - protected $urlGenerator; + private $urlGenerator; /** @var IL10N */ - protected $l; + private $l10n; + + /** @var ITimeFactory */ + private $timeFactory; - public function __construct(IFactory $factory, IURLGenerator $urlGenerator) { - $this->factory = $factory; + /** + * 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; } /** @@ -62,7 +75,7 @@ class Notifier implements INotifier { * @since 17.0.0 */ public function getID():string { - return 'dav'; + return Application::APP_ID; } /** @@ -72,89 +85,236 @@ class Notifier implements INotifier { * @since 17.0.0 */ public function getName():string { - return $this->factory->get('dav')->t('Calendar'); + if ($this->l10n) { + return $this->l10n->t('Calendar'); + } + + 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 { + 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->l = $this->factory->get('dav', $languageCode); + $this->l10n = $this->l10nFactory->get('dav', $languageCode); - if ($notification->getSubject() === 'calendar_reminder') { - $subjectParameters = $notification->getSubjectParameters(); - $notification->setParsedSubject($this->processEventTitle($subjectParameters)); + // Handle notifier subjects + switch($notification->getSubject()) { + case 'calendar_reminder': + return $this->prepareReminderNotification($notification); + + default: + throw new \InvalidArgumentException('Unknown subject'); - $messageParameters = $notification->getMessageParameters(); - $notification->setParsedMessage($this->processEventDescription($messageParameters)); - $notification->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'places/calendar.svg'))); - return $notification; } - // Unknown subject => Unknown notification => throw - throw new \InvalidArgumentException('Unknown subject'); } /** - * @param array $event - * @return string - * @throws \Exception + * @param INotification $notification + * @return INotification */ - private function processEventTitle(array $event):string { - $event_datetime = new \DateTime(); - $event_datetime->setTimestamp($event['start']); - $now = new \DateTime(); - - $diff = $event_datetime->diff($now); - - foreach (self::$units as $attribute => $unit) { - $count = $diff->$attribute; - if (0 !== $count) { - return $this->getPluralizedTitle($count, $diff->invert, $unit, $event['title']); - } - } - return ''; + 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 int $count - * @param int $invert - * @param string $unit - * @param string $title - * @return string + * @param INotification $notification */ - private function getPluralizedTitle(int $count, int $invert, string $unit, string $title):string { - if ($invert) { - return $this->l->n('%s (in one %s)', '%s (in %n %ss)', $count, [$title, $unit]); + private function prepareNotificationSubject(INotification $notification): void { + $parameters = $notification->getSubjectParameters(); + + $startTime = \DateTime::createFromFormat(\DateTimeInterface::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]); } - // This should probably not show up - return $this->l->n('%s (one %s ago)', '%s (%n %ss ago)', $count, [$title, $unit]); + + $notification->setParsedSubject($title); } /** - * @param array $event - * @return string + * Sets the notification message based on the parameters set in PushProvider + * + * @param INotification $notification */ - private function processEventDescription(array $event):string { + private function prepareNotificationMessage(INotification $notification): void { + $parameters = $notification->getMessageParameters(); + $description = [ - $this->l->t('Calendar: %s', $event['calendar']), - $this->l->t('Date: %s', $event['when']), + $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'); + } - if ($event['description']) { - $description[] = $this->l->t('Description: %s', $event['description']); + /** + * @param array $parameters + * @return string + * @throws \Exception + */ + private function generateDateString(array $parameters):string { + $startDateTime = DateTime::createFromFormat(DATE_ATOM, $parameters['start_atom']); + $endDateTime = DateTime::createFromFormat(DATE_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), + ]); } - if ($event['location']) { - $description[] = $this->l->t('Where: %s', $event['location']); + + $startTimezone = $endTimezone = null; + if (!$parameters['start_is_floating']) { + $startTimezone = $parameters['start_timezone']; + $endTimezone = $parameters['end_timezone']; } - return implode('<br>', $description); + + $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 index ce6c846061e..ad428eef745 100644 --- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php +++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php @@ -1,6 +1,11 @@ <?php +declare(strict_types=1); /** + * @copyright Copyright (c) 2019, Thomas Citharel + * @copyright Copyright (c) 2019, Georg Ehrke + * * @author Thomas Citharel <tcit@tcit.fr> + * @author Georg Ehrke <oc.list@georgehrke.com> * * @license AGPL-3.0 * @@ -19,14 +24,19 @@ */ namespace OCA\DAV\CalDAV\Reminder; -use OC\User\NoUserException; +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 OCP\IUserSession; use Sabre\VObject; use Sabre\VObject\Component\VAlarm; -use Sabre\VObject\Reader; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\ParseException; +use Sabre\VObject\Recur\EventIterator; +use Sabre\VObject\Recur\NoInstancesException; class ReminderService { @@ -42,144 +52,710 @@ class ReminderService { /** @var IGroupManager */ private $groupManager; - /** @var IUserSession */ - private $userSession; + /** @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, - IUserSession $userSession) { + CalDavBackend $caldavBackend, + ITimeFactory $timeFactory) { $this->backend = $backend; $this->notificationProviderManager = $notificationProviderManager; $this->userManager = $userManager; $this->groupManager = $groupManager; - $this->userSession = $userSession; + $this->caldavBackend = $caldavBackend; + $this->timeFactory = $timeFactory; } /** * Process reminders to activate * - * @throws NoUserException * @throws NotificationProvider\ProviderNotAvailableException * @throws NotificationTypeDoesNotExistException */ public function processReminders():void { $reminders = $this->backend->getRemindersToProcess(); - foreach ($reminders as $reminder) { - $calendarData = Reader::read($reminder['calendardata']); + 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; + } - $user = $this->userManager->get($reminder['uid']); + if ($this->wasEventCancelled($vevent)) { + $this->deleteOrProcessNext($reminder, $vevent); + continue; + } + + if (!$this->notificationProviderManager->hasProvider($reminder['type'])) { + $this->deleteOrProcessNext($reminder, $vevent); + continue; + } - if ($user === null) { - throw new NoUserException('User not found for calendar'); + $users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']); + $user = $this->getUserFromPrincipalURI($reminder['principaluri']); + if ($user) { + $users[] = $user; } $notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']); - $notificationProvider->send($calendarData, $reminder['displayname'], $user); - $this->backend->removeReminder($reminder['id']); + $notificationProvider->send($vevent, $reminder['displayname'], $users); + + $this->deleteOrProcessNext($reminder, $vevent); } } /** - * 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 - * @return void * @throws VObject\InvalidDataException - * @throws NoUserException */ - public function onTouchCalendarObject(string $action, array $calendarData, array $shares, array $objectData):void { - if (!isset($calendarData['principaluri'])) { + public function onTouchCalendarObject(string $action, + array $objectData):void { + // We only support VEvents for now + if (strcasecmp($objectData['component'], 'vevent') !== 0) { return; } - // Always remove existing reminders for this event - $this->backend->cleanRemindersForEvent($objectData['calendarid'], $objectData['uri']); + switch($action) { + case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject': + $this->onCalendarObjectCreate($objectData); + break; + + case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject': + $this->onCalendarObjectEdit($objectData); + break; - /** - * If we are deleting the event, no need to go further - */ - if ($action === '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject') { + 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; } - $user = $this->userSession->getUser(); + $vevents = $this->getAllVEventsFromVCalendar($vcalendar); + if (count($vevents) === 0) { + return; + } - if ($user === null) { - throw new NoUserException('No user in session'); + $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); + } } - $users = $this->getUsersForShares($shares); + if ($masterItem) { + $processedAlarms = []; + $masterAlarms = []; + $masterHash = $this->getEventHash($masterItem); + + foreach($masterItem->VALARM as $valarm) { + $masterAlarms[] = $this->getAlarmHash($valarm); + } - $users[] = $user->getUID(); + 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; + } - $vobject = VObject\Reader::read($objectData['calendardata']); + 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; + } - foreach ($vobject->VEVENT->VALARM as $alarm) { - if ($alarm instanceof VAlarm) { - $type = strtoupper($alarm->ACTION->getValue()); - if (in_array($type, self::REMINDER_TYPES, true)) { - $time = $alarm->getEffectiveTriggerTime(); + $triggerTime = $valarm->getEffectiveTriggerTime(); - foreach ($users as $uid) { - $this->backend->insertReminder( - $uid, - $objectData['calendarid'], - $objectData['uri'], - $type, - $time->getTimestamp(), - $vobject->VEVENT->DTSTART->getDateTime()->getTimestamp()); + // 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); + } /** - * Get all users that have access to a given calendar - * - * @param array $shares - * @return string[] + * @param array $objectData */ - private function getUsersForShares(array $shares):array { - $users = $groups = []; + 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 = $valarm->REPEAT ? (int) $valarm->REPEAT : 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') { - $users[] = $principal[2]; + $user = $this->userManager->get($principal[2]); + if ($user) { + $users[] = $user; + $userIds[] = $principal[2]; + } } else if ($principal[1] === 'groups') { $groups[] = $principal[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(); + 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 array_unique($users); + 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 (strcasecmp($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 index 02d8a287269..93477cb0f72 100644 --- a/apps/dav/lib/Command/SendEventReminders.php +++ b/apps/dav/lib/Command/SendEventReminders.php @@ -27,6 +27,11 @@ 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 */ @@ -35,10 +40,13 @@ class SendEventReminders extends Command { /** @var IConfig */ protected $config; - public function __construct(string $name = null, - ReminderService $reminderService, + /** + * @param ReminderService $reminderService + * @param IConfig $config + */ + public function __construct(ReminderService $reminderService, IConfig $config) { - parent::__construct($name); + parent::__construct(); $this->reminderService = $reminderService; $this->config = $config; } diff --git a/apps/dav/lib/Migration/Version1004Date20170825134824.php b/apps/dav/lib/Migration/Version1004Date20170825134824.php index 26855c2e23e..f3165a0fe3d 100644 --- a/apps/dav/lib/Migration/Version1004Date20170825134824.php +++ b/apps/dav/lib/Migration/Version1004Date20170825134824.php @@ -324,8 +324,7 @@ class Version1004Date20170825134824 extends SimpleMigrationStep { 'length' => 1, ]); $table->addColumn('stripattachments', 'smallint', [ - 'notnull' => false, - 'length' => 1, + ]); $table->addColumn('lastmodified', 'integer', [ 'notnull' => false, diff --git a/apps/dav/lib/Migration/Version1007Date20181005133326.php b/apps/dav/lib/Migration/Version1007Date20181005133326.php deleted file mode 100644 index 1e4cce950ac..00000000000 --- a/apps/dav/lib/Migration/Version1007Date20181005133326.php +++ /dev/null @@ -1,82 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace OCA\DAV\Migration; - -use Closure; -use OCP\DB\ISchemaWrapper; -use OCP\Migration\SimpleMigrationStep; -use OCP\Migration\IOutput; -use Doctrine\DBAL\Types\Type; - -/** - * Auto-generated migration step: Please modify to your needs! - */ -class Version1007Date20181005133326 extends SimpleMigrationStep { - - /** - * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` - * @param array $options - */ - public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options) { - } - - /** - * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` - * @param array $options - * @return null|ISchemaWrapper - */ - public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { - /** @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('uid', Type::STRING, [ - 'notnull' => true, - 'length' => 255, - ]); - $table->addColumn('calendarid', Type::BIGINT, [ - 'notnull' => false, - 'length' => 11, - ]); - $table->addColumn('objecturi', Type::STRING, [ - 'notnull' => true, - 'length' => 255, - ]); - $table->addColumn('type', Type::STRING, [ - 'notnull' => true, - 'length' => 255, - ]); - $table->addColumn('notificationdate', Type::DATETIME, [ - 'notnull' => false, - ]); - $table->addColumn('eventstartdate', Type::DATETIME, [ - 'notnull' => false, - ]); - - $table->setPrimaryKey(['id']); - $table->addIndex(['calendarid'], 'calendar_reminder_calendars'); - - return $schema; - } - } - - /** - * @param IOutput $output - * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` - * @param array $options - */ - public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) { - } -} diff --git a/apps/dav/lib/Migration/Version1012Date20190808122342.php b/apps/dav/lib/Migration/Version1012Date20190808122342.php new file mode 100644 index 00000000000..4aa768e705d --- /dev/null +++ b/apps/dav/lib/Migration/Version1012Date20190808122342.php @@ -0,0 +1,116 @@ +<?php +declare(strict_types=1); +/** + * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com> + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\DAV\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; + } + } +} |