2019-03-16 18:19:25 +03:00
|
|
|
<?php
|
2019-12-03 21:57:53 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
declare(strict_types=1);
|
2019-12-03 21:57:53 +03:00
|
|
|
|
2019-03-16 18:19:25 +03:00
|
|
|
/**
|
2019-08-09 21:25:21 +03:00
|
|
|
* @copyright Copyright (c) 2019, Thomas Citharel
|
|
|
|
* @copyright Copyright (c) 2019, Georg Ehrke
|
|
|
|
*
|
|
|
|
* @author Georg Ehrke <oc.list@georgehrke.com>
|
2019-12-03 21:57:53 +03:00
|
|
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
2020-03-31 11:49:10 +03:00
|
|
|
* @author Thomas Citharel <nextcloud@tcit.fr>
|
2019-03-16 18:19:25 +03:00
|
|
|
*
|
2019-12-03 21:57:53 +03:00
|
|
|
* @license GNU AGPL version 3 or any later version
|
2019-03-16 18:19:25 +03:00
|
|
|
*
|
2019-12-03 21:57:53 +03:00
|
|
|
* 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.
|
2019-03-16 18:19:25 +03:00
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
2019-12-03 21:57:53 +03:00
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
2019-03-16 18:19:25 +03:00
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
2019-12-03 21:57:53 +03:00
|
|
|
* 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/>.
|
2019-03-16 18:19:25 +03:00
|
|
|
*
|
|
|
|
*/
|
2019-11-22 22:52:10 +03:00
|
|
|
|
2019-03-16 18:19:25 +03:00
|
|
|
namespace OCA\DAV\CalDAV\Reminder;
|
|
|
|
|
2019-11-22 22:52:10 +03:00
|
|
|
use DateTimeImmutable;
|
2019-08-09 21:25:21 +03:00
|
|
|
use OCA\DAV\CalDAV\CalDavBackend;
|
|
|
|
use OCP\AppFramework\Utility\ITimeFactory;
|
2019-03-16 18:19:25 +03:00
|
|
|
use OCP\IGroup;
|
|
|
|
use OCP\IGroupManager;
|
2019-08-09 21:25:21 +03:00
|
|
|
use OCP\IUser;
|
2019-03-16 18:19:25 +03:00
|
|
|
use OCP\IUserManager;
|
|
|
|
use Sabre\VObject;
|
|
|
|
use Sabre\VObject\Component\VAlarm;
|
2019-08-09 21:25:21 +03:00
|
|
|
use Sabre\VObject\Component\VEvent;
|
|
|
|
use Sabre\VObject\ParseException;
|
|
|
|
use Sabre\VObject\Recur\EventIterator;
|
|
|
|
use Sabre\VObject\Recur\NoInstancesException;
|
2019-03-16 18:19:25 +03:00
|
|
|
|
|
|
|
class ReminderService {
|
|
|
|
|
2019-08-12 14:20:03 +03:00
|
|
|
/** @var Backend */
|
|
|
|
private $backend;
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-12 14:20:03 +03:00
|
|
|
/** @var NotificationProviderManager */
|
|
|
|
private $notificationProviderManager;
|
2019-03-16 18:19:25 +03:00
|
|
|
|
|
|
|
/** @var IUserManager */
|
|
|
|
private $userManager;
|
|
|
|
|
|
|
|
/** @var IGroupManager */
|
|
|
|
private $groupManager;
|
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
/** @var CalDavBackend */
|
|
|
|
private $caldavBackend;
|
|
|
|
|
|
|
|
/** @var ITimeFactory */
|
|
|
|
private $timeFactory;
|
2019-03-16 18:19:25 +03:00
|
|
|
|
|
|
|
public const REMINDER_TYPE_EMAIL = 'EMAIL';
|
|
|
|
public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
|
|
|
|
public const REMINDER_TYPE_AUDIO = 'AUDIO';
|
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
/**
|
|
|
|
* @var String[]
|
|
|
|
*
|
|
|
|
* Official RFC5545 reminder types
|
|
|
|
*/
|
2019-08-02 17:34:39 +03:00
|
|
|
public const REMINDER_TYPES = [
|
|
|
|
self::REMINDER_TYPE_EMAIL,
|
|
|
|
self::REMINDER_TYPE_DISPLAY,
|
|
|
|
self::REMINDER_TYPE_AUDIO
|
|
|
|
];
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
/**
|
|
|
|
* ReminderService constructor.
|
|
|
|
*
|
|
|
|
* @param Backend $backend
|
|
|
|
* @param NotificationProviderManager $notificationProviderManager
|
|
|
|
* @param IUserManager $userManager
|
|
|
|
* @param IGroupManager $groupManager
|
|
|
|
* @param CalDavBackend $caldavBackend
|
|
|
|
* @param ITimeFactory $timeFactory
|
|
|
|
*/
|
2019-08-12 14:20:03 +03:00
|
|
|
public function __construct(Backend $backend,
|
|
|
|
NotificationProviderManager $notificationProviderManager,
|
2019-03-16 18:19:25 +03:00
|
|
|
IUserManager $userManager,
|
|
|
|
IGroupManager $groupManager,
|
2019-08-09 21:25:21 +03:00
|
|
|
CalDavBackend $caldavBackend,
|
|
|
|
ITimeFactory $timeFactory) {
|
2019-08-12 14:20:03 +03:00
|
|
|
$this->backend = $backend;
|
|
|
|
$this->notificationProviderManager = $notificationProviderManager;
|
2019-03-16 18:19:25 +03:00
|
|
|
$this->userManager = $userManager;
|
|
|
|
$this->groupManager = $groupManager;
|
2019-08-09 21:25:21 +03:00
|
|
|
$this->caldavBackend = $caldavBackend;
|
|
|
|
$this->timeFactory = $timeFactory;
|
2019-08-12 14:20:03 +03:00
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Process reminders to activate
|
|
|
|
*
|
|
|
|
* @throws NotificationProvider\ProviderNotAvailableException
|
|
|
|
* @throws NotificationTypeDoesNotExistException
|
|
|
|
*/
|
2019-08-12 14:20:03 +03:00
|
|
|
public function processReminders():void {
|
|
|
|
$reminders = $this->backend->getRemindersToProcess();
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($reminders as $reminder) {
|
2019-11-26 14:20:35 +03:00
|
|
|
$calendarData = is_resource($reminder['calendardata'])
|
|
|
|
? stream_get_contents($reminder['calendardata'])
|
|
|
|
: $reminder['calendardata'];
|
|
|
|
|
|
|
|
$vcalendar = $this->parseCalendarData($calendarData);
|
2019-08-12 14:20:03 +03:00
|
|
|
if (!$vcalendar) {
|
2019-08-09 21:25:21 +03:00
|
|
|
$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;
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
if ($this->wasEventCancelled($vevent)) {
|
|
|
|
$this->deleteOrProcessNext($reminder, $vevent);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
|
|
|
|
$this->deleteOrProcessNext($reminder, $vevent);
|
|
|
|
continue;
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
|
|
|
|
$user = $this->getUserFromPrincipalURI($reminder['principaluri']);
|
|
|
|
if ($user) {
|
|
|
|
$users[] = $user;
|
2019-03-16 18:19:25 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
|
2019-08-09 21:25:21 +03:00
|
|
|
$notificationProvider->send($vevent, $reminder['displayname'], $users);
|
|
|
|
|
|
|
|
$this->deleteOrProcessNext($reminder, $vevent);
|
2019-03-16 18:19:25 +03:00
|
|
|
}
|
2019-08-12 14:20:03 +03:00
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $action
|
|
|
|
* @param array $objectData
|
|
|
|
* @throws VObject\InvalidDataException
|
|
|
|
*/
|
2019-08-09 21:25:21 +03:00
|
|
|
public function onTouchCalendarObject(string $action,
|
|
|
|
array $objectData):void {
|
|
|
|
// We only support VEvents for now
|
|
|
|
if (strcasecmp($objectData['component'], 'vevent') !== 0) {
|
2019-03-16 18:19:25 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
switch ($action) {
|
2019-08-09 21:25:21 +03:00
|
|
|
case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject':
|
|
|
|
$this->onCalendarObjectCreate($objectData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject':
|
|
|
|
$this->onCalendarObjectEdit($objectData);
|
|
|
|
break;
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
case '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject':
|
|
|
|
$this->onCalendarObjectDelete($objectData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $objectData
|
|
|
|
*/
|
2019-08-12 14:20:03 +03:00
|
|
|
private function onCalendarObjectCreate(array $objectData):void {
|
2019-11-26 14:20:35 +03:00
|
|
|
$calendarData = is_resource($objectData['calendardata'])
|
|
|
|
? stream_get_contents($objectData['calendardata'])
|
|
|
|
: $objectData['calendardata'];
|
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
/** @var VObject\Component\VCalendar $vcalendar */
|
2019-11-26 14:20:35 +03:00
|
|
|
$vcalendar = $this->parseCalendarData($calendarData);
|
2019-08-09 21:25:21 +03:00
|
|
|
if (!$vcalendar) {
|
2019-03-16 18:19:25 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
|
|
|
|
if (count($vevents) === 0) {
|
|
|
|
return;
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
$uid = (string) $vevents[0]->UID;
|
|
|
|
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
|
|
|
|
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
|
|
|
|
$now = $this->timeFactory->getDateTime();
|
|
|
|
$isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($recurrenceExceptions as $recurrenceException) {
|
2019-08-09 21:25:21 +03:00
|
|
|
$eventHash = $this->getEventHash($recurrenceException);
|
|
|
|
|
2019-08-18 21:56:57 +03:00
|
|
|
if (!isset($recurrenceException->VALARM)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($recurrenceException->VALARM as $valarm) {
|
2019-08-09 21:25:21 +03:00
|
|
|
/** @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);
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
}
|
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
if ($masterItem) {
|
|
|
|
$processedAlarms = [];
|
|
|
|
$masterAlarms = [];
|
|
|
|
$masterHash = $this->getEventHash($masterItem);
|
|
|
|
|
2019-08-18 21:56:57 +03:00
|
|
|
if (!isset($masterItem->VALARM)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($masterItem->VALARM as $valarm) {
|
2019-08-09 21:25:21 +03:00
|
|
|
$masterAlarms[] = $this->getAlarmHash($valarm);
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
|
2019-08-09 21:25:21 +03:00
|
|
|
$event = $iterator->getEventObject();
|
|
|
|
|
|
|
|
// Recurrence-exceptions are handled separately, so just ignore them here
|
|
|
|
if (\in_array($event, $recurrenceExceptions, true)) {
|
|
|
|
$iterator->next();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($event->VALARM as $valarm) {
|
2019-08-09 21:25:21 +03:00
|
|
|
/** @var VAlarm $valarm */
|
|
|
|
$alarmHash = $this->getAlarmHash($valarm);
|
|
|
|
if (\in_array($alarmHash, $processedAlarms, true)) {
|
|
|
|
continue;
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
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;
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
$triggerTime = $valarm->getEffectiveTriggerTime();
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
// 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;
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
continue;
|
2019-03-16 18:19:25 +03:00
|
|
|
}
|
2019-08-09 21:25:21 +03:00
|
|
|
|
|
|
|
$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
|
|
|
|
$this->writeRemindersToDatabase($alarms);
|
|
|
|
$processedAlarms[] = $alarmHash;
|
2019-03-16 18:19:25 +03:00
|
|
|
}
|
2019-08-09 21:25:21 +03:00
|
|
|
|
|
|
|
$iterator->next();
|
2019-03-16 18:19:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
/**
|
|
|
|
* @param array $objectData
|
|
|
|
*/
|
|
|
|
private function onCalendarObjectEdit(array $objectData):void {
|
|
|
|
// TODO - this can be vastly improved
|
|
|
|
// - get cached reminders
|
|
|
|
// - ...
|
|
|
|
|
|
|
|
$this->onCalendarObjectDelete($objectData);
|
|
|
|
$this->onCalendarObjectCreate($objectData);
|
|
|
|
}
|
2019-03-16 18:19:25 +03:00
|
|
|
|
|
|
|
/**
|
2019-08-09 21:25:21 +03:00
|
|
|
* @param array $objectData
|
2019-03-16 18:19:25 +03:00
|
|
|
*/
|
2019-08-09 21:25:21 +03:00
|
|
|
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,
|
|
|
|
];
|
|
|
|
|
2019-08-12 14:20:03 +03:00
|
|
|
$repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
|
2020-04-10 15:19:56 +03:00
|
|
|
for ($i = 0; $i < $repeat; $i++) {
|
2019-08-09 21:25:21 +03:00
|
|
|
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 {
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($reminders as $reminder) {
|
2019-08-09 21:25:21 +03:00
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
while ($iterator->valid()) {
|
2019-08-09 21:25:21 +03:00
|
|
|
$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;
|
|
|
|
}
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($event->VALARM as $valarm) {
|
2019-08-09 21:25:21 +03:00
|
|
|
/** @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 = [];
|
2019-03-16 18:19:25 +03:00
|
|
|
foreach ($shares as $share) {
|
2019-08-09 21:25:21 +03:00
|
|
|
// Only consider writable shares
|
|
|
|
if ($share['readOnly']) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-03-16 18:19:25 +03:00
|
|
|
$principal = explode('/', $share['{http://owncloud.org/ns}principal']);
|
|
|
|
if ($principal[1] === 'users') {
|
2019-08-09 21:25:21 +03:00
|
|
|
$user = $this->userManager->get($principal[2]);
|
|
|
|
if ($user) {
|
|
|
|
$users[] = $user;
|
|
|
|
$userIds[] = $principal[2];
|
|
|
|
}
|
2020-04-10 11:35:09 +03:00
|
|
|
} elseif ($principal[1] === 'groups') {
|
2019-03-16 18:19:25 +03:00
|
|
|
$groups[] = $principal[2];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
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();
|
2019-03-16 18:19:25 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-09 21:25:21 +03:00
|
|
|
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) {
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($recurrenceExceptions as $recurrenceException) {
|
2019-08-09 21:25:21 +03:00
|
|
|
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);
|
2020-04-10 15:19:56 +03:00
|
|
|
} catch (ParseException $ex) {
|
2019-08-09 21:25:21 +03:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $principalUri
|
|
|
|
* @return IUser|null
|
|
|
|
*/
|
|
|
|
private function getUserFromPrincipalURI(string $principalUri):?IUser {
|
|
|
|
if (!$principalUri) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-08-12 14:20:03 +03:00
|
|
|
if (stripos($principalUri, 'principals/users/') !== 0) {
|
2019-08-09 21:25:21 +03:00
|
|
|
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 = [];
|
|
|
|
|
2020-04-10 15:19:56 +03:00
|
|
|
foreach ($vcalendar->children() as $child) {
|
2019-08-09 21:25:21 +03:00
|
|
|
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 {
|
2020-04-09 14:53:40 +03:00
|
|
|
return array_values(array_filter($vevents, function (VEvent $vevent) {
|
2019-08-09 21:25:21 +03:00
|
|
|
return $vevent->{'RECURRENCE-ID'} !== null;
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $vevents
|
|
|
|
* @return VEvent|null
|
|
|
|
*/
|
|
|
|
private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
|
2020-04-09 14:53:40 +03:00
|
|
|
$elements = array_values(array_filter($vevents, function (VEvent $vevent) {
|
2019-08-09 21:25:21 +03:00
|
|
|
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);
|
2019-03-16 18:19:25 +03:00
|
|
|
}
|
|
|
|
}
|