123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782 |
- <?php
- declare(strict_types=1);
- /**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@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 DateTimeImmutable;
- use OCA\DAV\CalDAV\CalDavBackend;
- use OCP\AppFramework\Utility\ITimeFactory;
- use OCP\IGroup;
- use OCP\IGroupManager;
- use OCP\IUser;
- use OCP\IUserManager;
- use Sabre\VObject;
- use Sabre\VObject\Component\VAlarm;
- use Sabre\VObject\Component\VEvent;
- use Sabre\VObject\ParseException;
- use Sabre\VObject\Recur\EventIterator;
- use Sabre\VObject\Recur\NoInstancesException;
- class ReminderService {
- /** @var Backend */
- private $backend;
- /** @var NotificationProviderManager */
- private $notificationProviderManager;
- /** @var IUserManager */
- private $userManager;
- /** @var IGroupManager */
- private $groupManager;
- /** @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,
- CalDavBackend $caldavBackend,
- ITimeFactory $timeFactory) {
- $this->backend = $backend;
- $this->notificationProviderManager = $notificationProviderManager;
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->caldavBackend = $caldavBackend;
- $this->timeFactory = $timeFactory;
- }
- /**
- * Process reminders to activate
- *
- * @throws NotificationProvider\ProviderNotAvailableException
- * @throws NotificationTypeDoesNotExistException
- */
- public function processReminders():void {
- $reminders = $this->backend->getRemindersToProcess();
- foreach ($reminders as $reminder) {
- $calendarData = is_resource($reminder['calendardata'])
- ? stream_get_contents($reminder['calendardata'])
- : $reminder['calendardata'];
- $vcalendar = $this->parseCalendarData($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;
- }
- if ($this->wasEventCancelled($vevent)) {
- $this->deleteOrProcessNext($reminder, $vevent);
- continue;
- }
- if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
- $this->deleteOrProcessNext($reminder, $vevent);
- continue;
- }
- $users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
- $user = $this->getUserFromPrincipalURI($reminder['principaluri']);
- if ($user) {
- $users[] = $user;
- }
- $notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
- $notificationProvider->send($vevent, $reminder['displayname'], $users);
- $this->deleteOrProcessNext($reminder, $vevent);
- }
- }
- /**
- * @param string $action
- * @param array $objectData
- * @throws VObject\InvalidDataException
- */
- public function onTouchCalendarObject(string $action,
- array $objectData):void {
- // We only support VEvents for now
- if (strcasecmp($objectData['component'], 'vevent') !== 0) {
- return;
- }
- switch ($action) {
- case '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject':
- $this->onCalendarObjectCreate($objectData);
- break;
- case '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject':
- $this->onCalendarObjectEdit($objectData);
- break;
- case '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject':
- $this->onCalendarObjectDelete($objectData);
- break;
- default:
- break;
- }
- }
- /**
- * @param array $objectData
- */
- private function onCalendarObjectCreate(array $objectData):void {
- $calendarData = is_resource($objectData['calendardata'])
- ? stream_get_contents($objectData['calendardata'])
- : $objectData['calendardata'];
- /** @var VObject\Component\VCalendar $vcalendar */
- $vcalendar = $this->parseCalendarData($calendarData);
- if (!$vcalendar) {
- return;
- }
- $vevents = $this->getAllVEventsFromVCalendar($vcalendar);
- if (count($vevents) === 0) {
- return;
- }
- $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);
- if (!isset($recurrenceException->VALARM)) {
- continue;
- }
- 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);
- }
- }
- if ($masterItem) {
- $processedAlarms = [];
- $masterAlarms = [];
- $masterHash = $this->getEventHash($masterItem);
- if (!isset($masterItem->VALARM)) {
- return;
- }
- foreach ($masterItem->VALARM as $valarm) {
- $masterAlarms[] = $this->getAlarmHash($valarm);
- }
- 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;
- }
- 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;
- }
- $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) {
- // 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);
- }
- /**
- * @param array $objectData
- */
- 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 = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 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') {
- $user = $this->userManager->get($principal[2]);
- if ($user) {
- $users[] = $user;
- $userIds[] = $principal[2];
- }
- } elseif ($principal[1] === 'groups') {
- $groups[] = $principal[2];
- }
- }
- 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 $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 (stripos($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);
- }
- }
|