]> source.dussan.org Git - nextcloud-server.git/commitdiff
Support recurring events + repeating alarms
authorGeorg Ehrke <developer@georgehrke.com>
Fri, 9 Aug 2019 18:25:21 +0000 (20:25 +0200)
committerRoeland Jago Douma <roeland@famdouma.nl>
Thu, 15 Aug 2019 18:03:51 +0000 (20:03 +0200)
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
20 files changed:
apps/dav/appinfo/info.xml
apps/dav/composer/composer/autoload_classmap.php
apps/dav/composer/composer/autoload_static.php
apps/dav/lib/AppInfo/Application.php
apps/dav/lib/CalDAV/Reminder/AbstractNotificationProvider.php [deleted file]
apps/dav/lib/CalDAV/Reminder/Backend.php
apps/dav/lib/CalDAV/Reminder/INotificationProvider.php [new file with mode: 0644]
apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php [new file with mode: 0644]
apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php
apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php
apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php
apps/dav/lib/CalDAV/Reminder/Notifier.php
apps/dav/lib/CalDAV/Reminder/ReminderService.php
apps/dav/lib/Command/SendEventReminders.php
apps/dav/lib/Migration/Version1004Date20170825134824.php
apps/dav/lib/Migration/Version1007Date20181005133326.php [deleted file]
apps/dav/lib/Migration/Version1012Date20190808122342.php [new file with mode: 0644]

index 6e0219e91ddca791bba09a7c1aa9fa6d74047dce..81b93de055dd8747f65b6bc033e80aa8d8dff260 100644 (file)
@@ -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>
index bd26290cc9a456eea2bc4fede722341894ba6737..a1c2d671b8a2f2071eb308fac415999ee739c363 100644 (file)
@@ -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',
index 1a0ce9d02f02ea4ae4a1047223de250dc4aff148..39488419f87e9e93e2a154ab620755b84f699f1a 100644 (file)
@@ -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',
index 5d17dc5a241e499a922443e4bbdf1ce852503a73..80e9dea882902d14ad52f5865e9478561af6b4ff 100644 (file)
@@ -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 (file)
index 1d85862..0000000
+++ /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');
-       }
-}
index 087a5785f36d6e46abe7e0c736d82ded54c56008..be65c35da0f513cb054cfd159c69f56bc8eadcda 100644 (file)
@@ -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 (file)
index 0000000..d0e526e
--- /dev/null
@@ -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 (file)
index 0000000..6b2364c
--- /dev/null
@@ -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;
+       }
+}
index 6e702bcacaa91a2a82e23698f8b9b03db89fbdf2..ad4ac342f6697edb3df1e5498fb3a851a0a30565 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+declare(strict_types=1);
 /**
  * @copyright Copyright (c) 2019, Georg Ehrke
  *
index f05439932b623a49d349ef297722c4bbe2cf60f2..2a7eb2a4032292fb2bd480be54ab37af9bd01254 100644 (file)
@@ -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
  *
  * 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');
+       }
 }
index bf736db8a344729a13be0763ef5d8189c0e0a378..bfa6db958521f42f005fc9c347ad491f5f972e4a 100644 (file)
@@ -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 {
index f04b8e4c45a807c6e4a89c1527454a03e8f3cc83..2e580fd78a32b45091617737ebc10a02d607020c 100644 (file)
@@ -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
  *
  * 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(),
+               ];
+       }
 }
index 0a2579aedfb2a401b626eebdcb48ea58cbc06ec5..3d54970562d8965e11b2e66ec241ccaf3586246b 100644 (file)
@@ -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
  *
  */
 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');
                }
 
index ae4ec3bd3b76fff4921c72cba4e84236ebb4027f..c060089785a00dadd325b4acc08f2286636a05bb 100644 (file)
@@ -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>
  *
index 3718d5b29a6c0d5b50ab8f643c12ef2a4caf04e1..4bad9841787ef385701d00b8c4c86c03da0fb564 100644 (file)
@@ -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
  *
 
 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']);
        }
 }
index ce6c846061e94a6e1dc263d4a939e00da25037aa..ad428eef74585b9550dfb6f2b86cb87ba5dbb9c8 100644 (file)
@@ -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
  *
  */
 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);
        }
 }
index 02d8a28726901e068e48b1ed307a251b7ba1e93b..93477cb0f721a3ecb12afb8ffcb8256355e8d997 100644 (file)
@@ -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;
        }
index 26855c2e23e02a4247e8d5e2338248b18def9828..f3165a0fe3de0e0565ec100f522953548a3b9a16 100644 (file)
@@ -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 (file)
index 1e4cce9..0000000
+++ /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 (file)
index 0000000..4aa768e
--- /dev/null
@@ -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;
+               }
+       }
+}