aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/CalDAV
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/CalDAV')
-rw-r--r--apps/dav/lib/CalDAV/Activity/Backend.php59
-rw-r--r--apps/dav/lib/CalDAV/Activity/Filter/Calendar.php18
-rw-r--r--apps/dav/lib/CalDAV/Activity/Filter/Todo.php18
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Base.php31
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Calendar.php29
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Event.php46
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Todo.php8
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php8
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Calendar.php5
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Event.php5
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Todo.php5
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php8
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php14
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php13
-rw-r--r--apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php34
-rw-r--r--apps/dav/lib/CalDAV/BirthdayService.php49
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionImpl.php46
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionProvider.php2
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php726
-rw-r--r--apps/dav/lib/CalDAV/Calendar.php42
-rw-r--r--apps/dav/lib/CalDAV/CalendarHome.php24
-rw-r--r--apps/dav/lib/CalDAV/CalendarImpl.php122
-rw-r--r--apps/dav/lib/CalDAV/CalendarManager.php24
-rw-r--r--apps/dav/lib/CalDAV/CalendarObject.php15
-rw-r--r--apps/dav/lib/CalDAV/CalendarProvider.php62
-rw-r--r--apps/dav/lib/CalDAV/CalendarRoot.php9
-rw-r--r--apps/dav/lib/CalDAV/DefaultCalendarValidator.php41
-rw-r--r--apps/dav/lib/CalDAV/EmbeddedCalDavServer.php118
-rw-r--r--apps/dav/lib/CalDAV/EventComparisonService.php14
-rw-r--r--apps/dav/lib/CalDAV/EventReader.php771
-rw-r--r--apps/dav/lib/CalDAV/EventReaderRDate.php35
-rw-r--r--apps/dav/lib/CalDAV/EventReaderRRule.php87
-rw-r--r--apps/dav/lib/CalDAV/Export/ExportService.php107
-rw-r--r--apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php11
-rw-r--r--apps/dav/lib/CalDAV/Integration/ExternalCalendar.php14
-rw-r--r--apps/dav/lib/CalDAV/Integration/ICalendarProvider.php1
-rw-r--r--apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php37
-rw-r--r--apps/dav/lib/CalDAV/Outbox.php10
-rw-r--r--apps/dav/lib/CalDAV/Principal/Collection.php1
-rw-r--r--apps/dav/lib/CalDAV/Principal/User.php1
-rw-r--r--apps/dav/lib/CalDAV/Proxy/Proxy.php7
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendar.php1
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendarObject.php1
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendarRoot.php24
-rw-r--r--apps/dav/lib/CalDAV/Publishing/PublishPlugin.php45
-rw-r--r--apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php18
-rw-r--r--apps/dav/lib/CalDAV/Reminder/Backend.php51
-rw-r--r--apps/dav/lib/CalDAV/Reminder/INotificationProvider.php2
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php27
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php18
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php24
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php10
-rw-r--r--apps/dav/lib/CalDAV/Reminder/Notifier.php22
-rw-r--r--apps/dav/lib/CalDAV/Reminder/ReminderService.php127
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php56
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php1
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php1
-rw-r--r--apps/dav/lib/CalDAV/RetentionService.php24
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php152
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipService.php889
-rw-r--r--apps/dav/lib/CalDAV/Schedule/Plugin.php114
-rw-r--r--apps/dav/lib/CalDAV/Search/SearchPlugin.php10
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php1
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php1
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php1
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php1
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php1
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php1
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php1
-rw-r--r--apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php24
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Backend.php3
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Service.php4
-rw-r--r--apps/dav/lib/CalDAV/Status/StatusService.php32
-rw-r--r--apps/dav/lib/CalDAV/TimeZoneFactory.php213
-rw-r--r--apps/dav/lib/CalDAV/TimezoneService.php6
-rw-r--r--apps/dav/lib/CalDAV/TipBroker.php187
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php33
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php17
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/Plugin.php10
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php14
-rw-r--r--apps/dav/lib/CalDAV/UpcomingEvent.php69
-rw-r--r--apps/dav/lib/CalDAV/UpcomingEventsService.php86
-rw-r--r--apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php40
-rw-r--r--apps/dav/lib/CalDAV/WebcalCaching/Connection.php143
-rw-r--r--apps/dav/lib/CalDAV/WebcalCaching/Plugin.php6
-rw-r--r--apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php264
86 files changed, 4045 insertions, 1407 deletions
diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php
index c2018baab92..f0c49e6e28c 100644
--- a/apps/dav/lib/CalDAV/Activity/Backend.php
+++ b/apps/dav/lib/CalDAV/Activity/Backend.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -25,27 +26,13 @@ use Sabre\VObject\Reader;
*/
class Backend {
- /** @var IActivityManager */
- protected $activityManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
- /** @var IUserSession */
- protected $userSession;
-
- /** @var IAppManager */
- protected $appManager;
-
- /** @var IUserManager */
- protected $userManager;
-
- public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, IAppManager $appManager, IUserManager $userManager) {
- $this->activityManager = $activityManager;
- $this->groupManager = $groupManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
- $this->userManager = $userManager;
+ public function __construct(
+ protected IActivityManager $activityManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IAppManager $appManager,
+ protected IUserManager $userManager,
+ ) {
}
/**
@@ -133,7 +120,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('calendar', (int) $calendarData['id'])
+ ->setObject('calendar', (int)$calendarData['id'])
->setType('calendar')
->setAuthor($currentUser);
@@ -161,7 +148,7 @@ class Backend {
[
'actor' => $currentUser,
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -192,7 +179,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('calendar', (int) $calendarData['id'])
+ ->setObject('calendar', (int)$calendarData['id'])
->setType('calendar')
->setAuthor($currentUser);
@@ -217,7 +204,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -246,7 +233,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -288,7 +275,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -315,7 +302,7 @@ class Backend {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -393,7 +380,7 @@ class Backend {
[
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $properties['id'],
+ 'id' => (int)$properties['id'],
'uri' => $properties['uri'],
'name' => $properties['{DAV:}displayname'],
],
@@ -443,7 +430,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('calendar', (int) $calendarData['id'])
+ ->setObject('calendar', (int)$calendarData['id'])
->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo')
->setAuthor($currentUser);
@@ -460,7 +447,7 @@ class Backend {
$params = [
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -534,7 +521,7 @@ class Backend {
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
- ->setObject('calendar', (int) $targetCalendarData['id'])
+ ->setObject('calendar', (int)$targetCalendarData['id'])
->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo')
->setAuthor($currentUser);
@@ -551,12 +538,12 @@ class Backend {
$params = [
'actor' => $event->getAuthor(),
'sourceCalendar' => [
- 'id' => (int) $sourceCalendarData['id'],
+ 'id' => (int)$sourceCalendarData['id'],
'uri' => $sourceCalendarData['uri'],
'name' => $sourceCalendarData['{DAV:}displayname'],
],
'targetCalendar' => [
- 'id' => (int) $targetCalendarData['id'],
+ 'id' => (int)$targetCalendarData['id'],
'uri' => $targetCalendarData['uri'],
'name' => $targetCalendarData['{DAV:}displayname'],
],
@@ -603,9 +590,9 @@ class Backend {
}
if ($componentType === 'VEVENT') {
- return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'event'];
+ return ['id' => (string)$component->UID, 'name' => (string)$component->SUMMARY, 'type' => 'event'];
}
- return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'todo', 'status' => (string) $component->STATUS];
+ return ['id' => (string)$component->UID, 'name' => (string)$component->SUMMARY, 'type' => 'todo', 'status' => (string)$component->STATUS];
}
/**
diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php
index 6ac161a0a2c..78579ee84b7 100644
--- a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -11,15 +12,10 @@ use OCP\IURLGenerator;
class Calendar implements IFilter {
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
-
- public function __construct(IL10N $l, IURLGenerator $url) {
- $this->l = $l;
- $this->url = $url;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ ) {
}
/**
@@ -40,8 +36,8 @@ class Calendar implements IFilter {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php
index dfa05ae9099..b001f90c28d 100644
--- a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -11,15 +12,10 @@ use OCP\IURLGenerator;
class Todo implements IFilter {
- /** @var IL10N */
- protected $l;
-
- /** @var IURLGenerator */
- protected $url;
-
- public function __construct(IL10N $l, IURLGenerator $url) {
- $this->l = $l;
- $this->url = $url;
+ public function __construct(
+ protected IL10N $l,
+ protected IURLGenerator $url,
+ ) {
}
/**
@@ -40,8 +36,8 @@ class Todo implements IFilter {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Base.php b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
index a063a31d015..558abe0ca1a 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Base.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -15,27 +16,19 @@ use OCP\IURLGenerator;
use OCP\IUserManager;
abstract class Base implements IProvider {
- /** @var IUserManager */
- protected $userManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
/** @var string[] */
protected $groupDisplayNames = [];
- /** @var IURLGenerator */
- protected $url;
-
/**
* @param IUserManager $userManager
* @param IGroupManager $groupManager
- * @param IURLGenerator $urlGenerator
+ * @param IURLGenerator $url
*/
- public function __construct(IUserManager $userManager, IGroupManager $groupManager, IURLGenerator $urlGenerator) {
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->url = $urlGenerator;
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IGroupManager $groupManager,
+ protected IURLGenerator $url,
+ ) {
}
protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
@@ -48,18 +41,18 @@ abstract class Base implements IProvider {
* @return array
*/
protected function generateCalendarParameter($data, IL10N $l) {
- if ($data['uri'] === CalDavBackend::PERSONAL_CALENDAR_URI &&
- $data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
+ if ($data['uri'] === CalDavBackend::PERSONAL_CALENDAR_URI
+ && $data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
return [
'type' => 'calendar',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $l->t('Personal'),
];
}
return [
'type' => 'calendar',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $data['name'],
];
}
@@ -72,7 +65,7 @@ abstract class Base implements IProvider {
protected function generateLegacyCalendarParameter($id, $name) {
return [
'type' => 'calendar',
- 'id' => $id,
+ 'id' => (string)$id,
'name' => $name,
];
}
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
index a7509831f29..8c93ddae431 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
@@ -1,10 +1,12 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -27,18 +29,9 @@ class Calendar extends Base {
public const SUBJECT_UNSHARE_USER = 'calendar_user_unshare';
public const SUBJECT_UNSHARE_GROUP = 'calendar_group_unshare';
- /** @var IFactory */
- protected $languageFactory;
-
/** @var IL10N */
protected $l;
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
/**
* @param IFactory $languageFactory
* @param IURLGenerator $url
@@ -47,11 +40,15 @@ class Calendar extends Base {
* @param IGroupManager $groupManager
* @param IEventMerger $eventMerger
*/
- public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, IEventMerger $eventMerger) {
+ public function __construct(
+ protected IFactory $languageFactory,
+ IURLGenerator $url,
+ protected IManager $activityManager,
+ IUserManager $userManager,
+ IGroupManager $groupManager,
+ protected IEventMerger $eventMerger,
+ ) {
parent::__construct($userManager, $groupManager, $url);
- $this->languageFactory = $languageFactory;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
}
/**
@@ -59,12 +56,12 @@ class Calendar extends Base {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->l = $this->languageFactory->get('dav', $language);
@@ -122,7 +119,7 @@ class Calendar extends Base {
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_by') {
$subject = $this->l->t('{actor} unshared calendar {calendar} from group {group}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event);
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Event.php b/apps/dav/lib/CalDAV/Activity/Provider/Event.php
index 959c0a815dd..87551d7840b 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php
@@ -1,11 +1,12 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
-use OC_App;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
@@ -24,21 +25,9 @@ class Event extends Base {
public const SUBJECT_OBJECT_RESTORE = 'object_restore';
public const SUBJECT_OBJECT_DELETE = 'object_delete';
- /** @var IFactory */
- protected $languageFactory;
-
/** @var IL10N */
protected $l;
- /** @var IManager */
- protected $activityManager;
-
- /** @var IEventMerger */
- protected $eventMerger;
-
- /** @var IAppManager */
- protected $appManager;
-
/**
* @param IFactory $languageFactory
* @param IURLGenerator $url
@@ -48,12 +37,16 @@ class Event extends Base {
* @param IEventMerger $eventMerger
* @param IAppManager $appManager
*/
- public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, IEventMerger $eventMerger, IAppManager $appManager) {
+ public function __construct(
+ protected IFactory $languageFactory,
+ IURLGenerator $url,
+ protected IManager $activityManager,
+ IUserManager $userManager,
+ IGroupManager $groupManager,
+ protected IEventMerger $eventMerger,
+ protected IAppManager $appManager,
+ ) {
parent::__construct($userManager, $groupManager, $url);
- $this->languageFactory = $languageFactory;
- $this->activityManager = $activityManager;
- $this->eventMerger = $eventMerger;
- $this->appManager = $appManager;
}
/**
@@ -74,7 +67,7 @@ class Event extends Base {
if (isset($eventData['link']) && is_array($eventData['link']) && $this->appManager->isEnabledForUser('calendar')) {
try {
// The calendar app needs to be manually loaded for the routes to be loaded
- OC_App::loadApp('calendar');
+ $this->appManager->loadApp('calendar');
$linkData = $eventData['link'];
$calendarUri = $this->urlencodeLowerHex($linkData['calendar_uri']);
if ($affectedUser === $linkData['owner']) {
@@ -86,14 +79,9 @@ class Event extends Base {
// as seen from the affected user.
$objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $affectedUser . '/' . $calendarUri . '_shared_by_' . $linkData['owner'] . '/' . $linkData['object_uri']);
}
- $link = [
- 'view' => 'dayGridMonth',
- 'timeRange' => 'now',
- 'mode' => 'sidebar',
+ $params['link'] = $this->url->linkToRouteAbsolute('calendar.view.indexdirect.edit', [
'objectId' => $objectId,
- 'recurrenceId' => 'next'
- ];
- $params['link'] = $this->url->linkToRouteAbsolute('calendar.view.indexview.timerange.edit', $link);
+ ]);
} catch (\Exception $error) {
// Do nothing
}
@@ -106,12 +94,12 @@ class Event extends Base {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar_event') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->l = $this->languageFactory->get('dav', $language);
@@ -147,7 +135,7 @@ class Event extends Base {
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event_self') {
$subject = $this->l->t('You restored event {event} of calendar {calendar}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event);
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
index 68a95341ff2..fc0625ec970 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
@@ -1,10 +1,12 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
+use OCP\Activity\Exceptions\UnknownActivityException;
use OCP\Activity\IEvent;
class Todo extends Event {
@@ -14,12 +16,12 @@ class Todo extends Event {
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
- * @throws \InvalidArgumentException
+ * @throws UnknownActivityException
* @since 11.0.0
*/
public function parse($language, IEvent $event, ?IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar_todo') {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$this->l = $this->languageFactory->get('dav', $language);
@@ -55,7 +57,7 @@ class Todo extends Event {
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo_self') {
$subject = $this->l->t('You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}');
} else {
- throw new \InvalidArgumentException();
+ throw new UnknownActivityException();
}
$parsedParameters = $this->getParameters($event);
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php b/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
index 67cc2abc9fd..7ab7f16dbbb 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
@@ -12,14 +12,12 @@ use OCP\Activity\ActivitySettings;
use OCP\IL10N;
abstract class CalDAVSetting extends ActivitySettings {
- /** @var IL10N */
- protected $l;
-
/**
* @param IL10N $l
*/
- public function __construct(IL10N $l) {
- $this->l = $l;
+ public function __construct(
+ protected IL10N $l,
+ ) {
}
public function getGroupIdentifier() {
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php
index 4e1fdc0fe97..0ad86a919bc 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -24,8 +25,8 @@ class Calendar extends CalDAVSetting {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Event.php b/apps/dav/lib/CalDAV/Activity/Setting/Event.php
index a177d27d2e3..ea9476d6f08 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Event.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Event.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -24,8 +25,8 @@ class Event extends CalDAVSetting {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php
index ecd3634ee12..ed8377b0ffa 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -25,8 +26,8 @@ class Todo extends CalDAVSetting {
/**
* @return int whether the filter should be rather on the top or bottom of
- * the admin section. The filters are arranged in ascending order of the
- * priority values. It is required to return a value between 0 and 100.
+ * the admin section. The filters are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
index 2d9beff9ced..87d26324c32 100644
--- a/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
@@ -24,12 +24,14 @@ use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Reader;
class AppCalendar extends ExternalCalendar {
- protected string $principal;
protected ICalendar $calendar;
- public function __construct(string $appId, ICalendar $calendar, string $principal) {
+ public function __construct(
+ string $appId,
+ ICalendar $calendar,
+ protected string $principal,
+ ) {
parent::__construct($appId, $calendar->getUri());
- $this->principal = $principal;
$this->calendar = $calendar;
}
diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
index 56e91c581f8..72f2ed2c163 100644
--- a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
@@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\AppCalendar;
+use OCA\DAV\CalDAV\CachedSubscriptionImpl;
+use OCA\DAV\CalDAV\CalendarImpl;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCP\Calendar\IManager;
@@ -16,12 +18,10 @@ use Psr\Log\LoggerInterface;
/* Plugin for wrapping application generated calendars registered in nextcloud core (OCP\Calendar\ICalendarProvider) */
class AppCalendarPlugin implements ICalendarProvider {
- protected IManager $manager;
- protected LoggerInterface $logger;
-
- public function __construct(IManager $manager, LoggerInterface $logger) {
- $this->manager = $manager;
- $this->logger = $logger;
+ public function __construct(
+ protected IManager $manager,
+ protected LoggerInterface $logger,
+ ) {
}
public function getAppID(): string {
@@ -51,7 +51,7 @@ class AppCalendarPlugin implements ICalendarProvider {
return array_values(
array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) {
// We must not provide a wrapper for DAV calendars
- return ! (($c instanceof \OCA\DAV\CalDAV\CalendarImpl) || ($c instanceof \OCA\DAV\CalDAV\CachedSubscriptionImpl));
+ return ! (($c instanceof CalendarImpl) || ($c instanceof CachedSubscriptionImpl));
})
);
}
diff --git a/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
index ba3f7074faf..3c62a26df54 100644
--- a/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
+++ b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
@@ -20,14 +20,11 @@ use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Property\ICalendar\DateTime;
class CalendarObject implements ICalendarObject, IACL {
- private VCalendar $vobject;
- private AppCalendar $calendar;
- private ICalendar|ICreateFromString $backend;
-
- public function __construct(AppCalendar $calendar, ICalendar $backend, VCalendar $vobject) {
- $this->backend = $backend;
- $this->calendar = $calendar;
- $this->vobject = $vobject;
+ public function __construct(
+ private AppCalendar $calendar,
+ private ICalendar|ICreateFromString $backend,
+ private VCalendar $vobject,
+ ) {
}
public function getOwner() {
diff --git a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
index 733e8079111..681709cdb6f 100644
--- a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
+++ b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -7,6 +8,7 @@ namespace OCA\DAV\CalDAV\BirthdayCalendar;
use OCA\DAV\CalDAV\BirthdayService;
use OCA\DAV\CalDAV\CalendarHome;
+use OCP\AppFramework\Http;
use OCP\IConfig;
use OCP\IUser;
use Sabre\DAV\Server;
@@ -24,23 +26,10 @@ class EnablePlugin extends ServerPlugin {
public const NS_Nextcloud = 'http://nextcloud.com/ns';
/**
- * @var IConfig
- */
- protected $config;
-
- /**
- * @var BirthdayService
- */
- protected $birthdayService;
-
- /**
* @var Server
*/
protected $server;
- /** @var IUser */
- private $user;
-
/**
* PublishPlugin constructor.
*
@@ -48,10 +37,11 @@ class EnablePlugin extends ServerPlugin {
* @param BirthdayService $birthdayService
* @param IUser $user
*/
- public function __construct(IConfig $config, BirthdayService $birthdayService, IUser $user) {
- $this->config = $config;
- $this->birthdayService = $birthdayService;
- $this->user = $user;
+ public function __construct(
+ protected IConfig $config,
+ protected BirthdayService $birthdayService,
+ private IUser $user,
+ ) {
}
/**
@@ -104,26 +94,26 @@ class EnablePlugin extends ServerPlugin {
*/
public function httpPost(RequestInterface $request, ResponseInterface $response) {
$node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
- if (!($node instanceof CalendarHome)) {
+ if (!$node instanceof CalendarHome) {
return;
}
$requestBody = $request->getBodyAsString();
$this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
- if ($documentType !== '{'.self::NS_Nextcloud.'}enable-birthday-calendar') {
+ if ($documentType !== '{' . self::NS_Nextcloud . '}enable-birthday-calendar') {
return;
}
$owner = substr($node->getOwner(), 17);
- if($owner !== $this->user->getUID()) {
- $this->server->httpResponse->setStatus(403);
+ if ($owner !== $this->user->getUID()) {
+ $this->server->httpResponse->setStatus(Http::STATUS_FORBIDDEN);
return false;
}
$this->config->setUserValue($this->user->getUID(), 'dav', 'generateBirthdayCalendar', 'yes');
$this->birthdayService->syncUser($this->user->getUID());
- $this->server->httpResponse->setStatus(204);
+ $this->server->httpResponse->setStatus(Http::STATUS_NO_CONTENT);
return false;
}
diff --git a/apps/dav/lib/CalDAV/BirthdayService.php b/apps/dav/lib/CalDAV/BirthdayService.php
index 0990cd674dd..680b228766f 100644
--- a/apps/dav/lib/CalDAV/BirthdayService.php
+++ b/apps/dav/lib/CalDAV/BirthdayService.php
@@ -32,28 +32,17 @@ class BirthdayService {
public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR';
- private GroupPrincipalBackend $principalBackend;
- private CalDavBackend $calDavBackEnd;
- private CardDavBackend $cardDavBackEnd;
- private IConfig $config;
- private IDBConnection $dbConnection;
- private IL10N $l10n;
-
/**
* BirthdayService constructor.
*/
- public function __construct(CalDavBackend $calDavBackEnd,
- CardDavBackend $cardDavBackEnd,
- GroupPrincipalBackend $principalBackend,
- IConfig $config,
- IDBConnection $dbConnection,
- IL10N $l10n) {
- $this->calDavBackEnd = $calDavBackEnd;
- $this->cardDavBackEnd = $cardDavBackEnd;
- $this->principalBackend = $principalBackend;
- $this->config = $config;
- $this->dbConnection = $dbConnection;
- $this->l10n = $l10n;
+ public function __construct(
+ private CalDavBackend $calDavBackEnd,
+ private CardDavBackend $cardDavBackEnd,
+ private GroupPrincipalBackend $principalBackend,
+ private IConfig $config,
+ private IDBConnection $dbConnection,
+ private IL10N $l10n,
+ ) {
}
public function onCardChanged(int $addressBookId,
@@ -87,7 +76,7 @@ class BirthdayService {
return;
}
foreach ($datesToSync as $type) {
- $this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type, $reminderOffset);
+ $this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset);
}
}
}
@@ -108,7 +97,7 @@ class BirthdayService {
$calendar = $this->ensureCalendarExists($principalUri);
foreach (['', '-death', '-anniversary'] as $tag) {
- $objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics';
+ $objectUri = $book['uri'] . '-' . $cardUri . $tag . '.ics';
$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
}
}
@@ -139,9 +128,9 @@ class BirthdayService {
* @return VCalendar|null
* @throws InvalidDataException
*/
- public function buildDateFromContact(string $cardData,
- string $dateField,
- string $postfix,
+ public function buildDateFromContact(string $cardData,
+ string $dateField,
+ string $postfix,
?string $reminderOffset):?VCalendar {
if (empty($cardData)) {
return null;
@@ -247,7 +236,7 @@ class BirthdayService {
$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
$vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $dateParts['year'] === null ? '1' : '0';
if ($originalYear !== null) {
- $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string) $originalYear;
+ $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string)$originalYear;
}
if ($reminderOffset) {
$alarm = $vCal->createComponent('VALARM');
@@ -264,7 +253,7 @@ class BirthdayService {
* @param string $user
*/
public function resetForUser(string $user):void {
- $principal = 'principals/users/'.$user;
+ $principal = 'principals/users/' . $user;
$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
if (!$calendar) {
return; // The user's birthday calendar doesn't exist, no need to purge it
@@ -281,13 +270,13 @@ class BirthdayService {
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function syncUser(string $user):void {
- $principal = 'principals/users/'.$user;
+ $principal = 'principals/users/' . $user;
$this->ensureCalendarExists($principal);
$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
foreach ($books as $book) {
$cards = $this->cardDavBackEnd->getCards($book['id']);
foreach ($cards as $card) {
- $this->onCardChanged((int) $book['id'], $card['uri'], $card['carddata']);
+ $this->onCardChanged((int)$book['id'], $card['uri'], $card['carddata']);
}
}
}
@@ -306,8 +295,8 @@ class BirthdayService {
}
return (
- $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() ||
- $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
+ $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue()
+ || $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
);
}
diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php
index 3f6eff4e1f4..cc1bab6d4fc 100644
--- a/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php
+++ b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php
@@ -9,22 +9,19 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV;
use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICalendarIsEnabled;
+use OCP\Calendar\ICalendarIsShared;
+use OCP\Calendar\ICalendarIsWritable;
use OCP\Constants;
-class CachedSubscriptionImpl implements ICalendar {
- private CalDavBackend $backend;
- private CachedSubscription $calendar;
- /** @var array<string, mixed> */
- private array $calendarInfo;
+class CachedSubscriptionImpl implements ICalendar, ICalendarIsEnabled, ICalendarIsShared, ICalendarIsWritable {
public function __construct(
- CachedSubscription $calendar,
- array $calendarInfo,
- CalDavBackend $backend
+ private CachedSubscription $calendar,
+ /** @var array<string, mixed> */
+ private array $calendarInfo,
+ private CalDavBackend $backend,
) {
- $this->calendar = $calendar;
- $this->calendarInfo = $calendarInfo;
- $this->backend = $backend;
}
/**
@@ -32,7 +29,7 @@ class CachedSubscriptionImpl implements ICalendar {
* @since 13.0.0
*/
public function getKey(): string {
- return (string) $this->calendarInfo['id'];
+ return (string)$this->calendarInfo['id'];
}
/**
@@ -58,16 +55,6 @@ class CachedSubscriptionImpl implements ICalendar {
return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color'];
}
- /**
- * @param string $pattern which should match within the $searchProperties
- * @param array $searchProperties defines the properties within the query pattern should match
- * @param array $options - optional parameters:
- * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
- * @param int|null $limit - limit number of search results
- * @param int|null $offset - offset for paging of search results
- * @return array an array of events/journals/todos which are arrays of key-value-pairs
- * @since 13.0.0
- */
public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array {
return $this->backend->search($this->calendarInfo, $pattern, $searchProperties, $options, $limit, $offset);
}
@@ -90,10 +77,25 @@ class CachedSubscriptionImpl implements ICalendar {
return $result;
}
+ /**
+ * @since 32.0.0
+ */
+ public function isEnabled(): bool {
+ return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true;
+ }
+
+ public function isWritable(): bool {
+ return false;
+ }
+
public function isDeleted(): bool {
return false;
}
+ public function isShared(): bool {
+ return true;
+ }
+
public function getSource(): string {
return $this->calendarInfo['source'];
}
diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php b/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php
index 73fde0b71a0..d64f039d05b 100644
--- a/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php
+++ b/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php
@@ -13,7 +13,7 @@ use OCP\Calendar\ICalendarProvider;
class CachedSubscriptionProvider implements ICalendarProvider {
public function __construct(
- private CalDavBackend $calDavBackend
+ private CalDavBackend $calDavBackend,
) {
}
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php
index cc6c4344c3c..d5b0d875ede 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
@@ -9,6 +10,7 @@ namespace OCA\DAV\CalDAV;
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
+use Generator;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\CalDAV\Sharing\Backend;
use OCA\DAV\Connector\Sabre\Principal;
@@ -19,12 +21,6 @@ use OCA\DAV\Events\CachedCalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarCreatedEvent;
use OCA\DAV\Events\CalendarDeletedEvent;
use OCA\DAV\Events\CalendarMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectCreatedEvent;
-use OCA\DAV\Events\CalendarObjectDeletedEvent;
-use OCA\DAV\Events\CalendarObjectMovedEvent;
-use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
-use OCA\DAV\Events\CalendarObjectRestoredEvent;
-use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarPublishedEvent;
use OCA\DAV\Events\CalendarRestoredEvent;
use OCA\DAV\Events\CalendarShareUpdatedEvent;
@@ -34,6 +30,13 @@ use OCA\DAV\Events\SubscriptionCreatedEvent;
use OCA\DAV\Events\SubscriptionDeletedEvent;
use OCA\DAV\Events\SubscriptionUpdatedEvent;
use OCP\AppFramework\Db\TTransactional;
+use OCP\Calendar\CalendarExportOptions;
+use OCP\Calendar\Events\CalendarObjectCreatedEvent;
+use OCP\Calendar\Events\CalendarObjectDeletedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedEvent;
+use OCP\Calendar\Events\CalendarObjectMovedToTrashEvent;
+use OCP\Calendar\Events\CalendarObjectRestoredEvent;
+use OCP\Calendar\Events\CalendarObjectUpdatedEvent;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
@@ -65,6 +68,8 @@ use Sabre\VObject\ParseException;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use Sabre\VObject\Recur\EventIterator;
+use Sabre\VObject\Recur\MaxInstancesExceededException;
+use Sabre\VObject\Recur\NoInstancesException;
use function array_column;
use function array_map;
use function array_merge;
@@ -86,6 +91,19 @@ use function time;
* Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
*
* @package OCA\DAV\CalDAV
+ *
+ * @psalm-type CalendarInfo = array{
+ * id: int,
+ * uri: string,
+ * principaluri: string,
+ * '{http://calendarserver.org/ns/}getctag': string,
+ * '{http://sabredav.org/ns}sync-token': int,
+ * '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet,
+ * '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': \Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp,
+ * '{DAV:}displayname': string,
+ * '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string,
+ * '{http://nextcloud.com/ns}owner-displayname': string,
+ * }
*/
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
use TTransactional;
@@ -176,7 +194,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/
protected array $userDisplayNames;
+ private string $dbObjectsTable = 'calendarobjects';
private string $dbObjectPropertiesTable = 'calendarobjects_props';
+ private string $dbObjectInvitationsTable = 'calendar_invitations';
private array $cachedObjects = [];
public function __construct(
@@ -193,15 +213,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * Return the number of calendars for a principal
+ * Return the number of calendars owned by the given principal.
*
- * By default this excludes the automatically generated birthday calendar
+ * Calendars shared with the given principal are not counted!
*
- * @param $principalUri
- * @param bool $excludeBirthday
- * @return int
+ * By default, this excludes the automatically generated birthday calendar.
*/
- public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
+ public function getCalendarsForUserCount(string $principalUri, bool $excludeBirthday = true): int {
$principalUri = $this->convertPrincipal($principalUri, true);
$query = $this->db->getQueryBuilder();
$query->select($query->func()->count('*'))
@@ -257,8 +275,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$calendars = [];
while (($row = $result->fetch()) !== false) {
$calendars[] = [
- 'id' => (int) $row['id'],
- 'deleted_at' => (int) $row['deleted_at'],
+ 'id' => (int)$row['id'],
+ 'deleted_at' => (int)$row['deleted_at'],
];
}
$result->closeCursor();
@@ -318,7 +336,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$calendars = [];
while ($row = $result->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$components = [];
if ($row['components']) {
$components = explode(',', $row['components']);
@@ -328,8 +346,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
@@ -352,7 +370,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$fields = array_column($this->propertyMap, 0);
$fields = array_map(function (string $field) {
- return 'a.'.$field;
+ return 'a.' . $field;
}, $fields);
$fields[] = 'a.id';
$fields[] = 'a.uri';
@@ -381,19 +399,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
while ($row = $results->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
if ($row['principaluri'] === $principalUri) {
continue;
}
- $readOnly = (int) $row['access'] === Backend::ACCESS_READ;
+ $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
if (isset($calendars[$row['id']])) {
if ($readOnly) {
// New share can not have more permissions than the old one.
continue;
}
- if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
- $calendars[$row['id']][$readOnlyPropertyName] === 0) {
+ if (isset($calendars[$row['id']][$readOnlyPropertyName])
+ && $calendars[$row['id']][$readOnlyPropertyName] === 0) {
// Old share is already read-write, no more permissions can be gained
continue;
}
@@ -410,8 +428,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $uri,
'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
@@ -451,7 +469,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$stmt = $query->executeQuery();
$calendars = [];
while ($row = $stmt->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$components = [];
if ($row['components']) {
$components = explode(',', $row['components']);
@@ -460,8 +478,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
];
@@ -501,7 +519,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->executeQuery();
while ($row = $result->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
[, $name] = Uri\split($row['principaluri']);
$row['displayname'] = $row['displayname'] . "($name)";
$components = [];
@@ -512,8 +530,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['publicuri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
@@ -566,7 +584,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
throw new NotFound('Node with name \'' . $uri . '\' could not be found');
}
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
[, $name] = Uri\split($row['principaluri']);
$row['displayname'] = $row['displayname'] . ' ' . "($name)";
$components = [];
@@ -577,8 +595,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['publicuri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
@@ -621,7 +639,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$components = [];
if ($row['components']) {
$components = explode(',', $row['components']);
@@ -631,8 +649,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
];
@@ -645,7 +663,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null
+ * @psalm-return CalendarInfo|null
+ * @return array|null
*/
public function getCalendarById(int $calendarId): ?array {
$fields = array_column($this->propertyMap, 0);
@@ -669,7 +688,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$components = [];
if ($row['components']) {
$components = explode(',', $row['components']);
@@ -679,7 +698,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'id' => $row['id'],
'uri' => $row['uri'],
'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
- '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
+ '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
'{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0,
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
@@ -717,7 +736,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
- $row['principaluri'] = (string) $row['principaluri'];
+ $row['principaluri'] = (string)$row['principaluri'];
$subscription = [
'id' => $row['id'],
'uri' => $row['uri'],
@@ -725,7 +744,44 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'source' => $row['source'],
'lastmodified' => $row['lastmodified'],
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
+ ];
+
+ return $this->rowToSubscription($row, $subscription);
+ }
+
+ public function getSubscriptionByUri(string $principal, string $uri): ?array {
+ $fields = array_column($this->subscriptionPropertyMap, 0);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'source';
+ $fields[] = 'synctoken';
+ $fields[] = 'principaluri';
+ $fields[] = 'lastmodified';
+
+ $query = $this->db->getQueryBuilder();
+ $query->select($fields)
+ ->from('calendarsubscriptions')
+ ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
+ ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
+ ->setMaxResults(1);
+ $stmt = $query->executeQuery();
+
+ $row = $stmt->fetch();
+ $stmt->closeCursor();
+ if ($row === false) {
+ return null;
+ }
+
+ $row['principaluri'] = (string)$row['principaluri'];
+ $subscription = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $row['principaluri'],
+ 'source' => $row['source'],
+ 'lastmodified' => $row['lastmodified'],
+ '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
];
return $this->rowToSubscription($row, $subscription);
@@ -754,7 +810,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'uri' => $calendarUri,
'synctoken' => 1,
'transparent' => 0,
- 'components' => 'VEVENT,VTODO',
+ 'components' => 'VEVENT,VTODO,VJOURNAL',
'displayname' => $calendarUri
];
@@ -773,7 +829,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
if (isset($properties[$transp])) {
- $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent');
+ $values['transparent'] = (int)($properties[$transp]->getValue() === 'transparent');
}
foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
@@ -826,7 +882,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
switch ($propertyName) {
case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
$fieldName = 'transparent';
- $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent');
+ $newValues[$fieldName] = (int)($propertyValue->getValue() === 'transparent');
break;
default:
$fieldName = $this->propertyMap[$propertyName][0];
@@ -843,7 +899,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
$query->executeStatement();
- $this->addChanges($calendarId, [""], 2);
+ $this->addChanges($calendarId, [''], 2);
$calendarData = $this->getCalendarById($calendarId);
$shares = $this->getShares($calendarId);
@@ -863,7 +919,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
- $this->atomic(function () use ($calendarId, $forceDeletePermanently) {
+ $this->atomic(function () use ($calendarId, $forceDeletePermanently): void {
// The calendar is deleted right away if this is either enforced by the caller
// or the special contacts birthday calendar or when the preference of an empty
// retention (0 seconds) is set, which signals a disabled trashbin.
@@ -874,6 +930,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$calendarData = $this->getCalendarById($calendarId);
$shares = $this->getShares($calendarId);
+ $this->purgeCalendarInvitations($calendarId);
+
$qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
$qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
@@ -924,7 +982,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
public function restoreCalendar(int $id): void {
- $this->atomic(function () use ($id) {
+ $this->atomic(function () use ($id): void {
$qb = $this->db->getQueryBuilder();
$update = $qb->update('calendars')
->set('deleted_at', $qb->createNamedParameter(null))
@@ -945,6 +1003,81 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * Returns all calendar entries as a stream of data
+ *
+ * @since 32.0.0
+ *
+ * @return Generator<array>
+ */
+ public function exportCalendar(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR, ?CalendarExportOptions $options = null): Generator {
+ // extract options
+ $rangeStart = $options?->getRangeStart();
+ $rangeCount = $options?->getRangeCount();
+ // construct query
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('*')
+ ->from('calendarobjects')
+ ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
+ ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
+ ->andWhere($qb->expr()->isNull('deleted_at'));
+ if ($rangeStart !== null) {
+ $qb->andWhere($qb->expr()->gt('uid', $qb->createNamedParameter($rangeStart)));
+ }
+ if ($rangeCount !== null) {
+ $qb->setMaxResults($rangeCount);
+ }
+ if ($rangeStart !== null || $rangeCount !== null) {
+ $qb->orderBy('uid', 'ASC');
+ }
+ $rs = $qb->executeQuery();
+ // iterate through results
+ try {
+ while (($row = $rs->fetch()) !== false) {
+ yield $row;
+ }
+ } finally {
+ $rs->closeCursor();
+ }
+ }
+
+ /**
+ * Returns all calendar objects with limited metadata for a calendar
+ *
+ * Every item contains an array with the following keys:
+ * * id - the table row id
+ * * etag - An arbitrary string
+ * * uri - a unique key which will be used to construct the uri. This can
+ * be any arbitrary string.
+ * * calendardata - The iCalendar-compatible calendar data
+ *
+ * @param mixed $calendarId
+ * @param int $calendarType
+ * @return array
+ */
+ public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
+ $query = $this->db->getQueryBuilder();
+ $query->select(['id','uid', 'etag', 'uri', 'calendardata'])
+ ->from('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
+ ->andWhere($query->expr()->isNull('deleted_at'));
+ $stmt = $query->executeQuery();
+
+ $result = [];
+ while (($row = $stmt->fetch()) !== false) {
+ $result[$row['uid']] = [
+ 'id' => $row['id'],
+ 'etag' => $row['etag'],
+ 'uri' => $row['uri'],
+ 'calendardata' => $row['calendardata'],
+ ];
+ }
+ $stmt->closeCursor();
+
+ return $result;
+ }
+
+ /**
* Delete all of an user's shares
*
* @param string $principaluri
@@ -1029,12 +1162,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'uri' => $row['uri'],
'lastmodified' => $row['lastmodified'],
'etag' => '"' . $row['etag'] . '"',
- 'calendarid' => (int) $row['calendarid'],
- 'calendartype' => (int) $row['calendartype'],
- 'size' => (int) $row['size'],
+ 'calendarid' => (int)$row['calendarid'],
+ 'calendartype' => (int)$row['calendartype'],
+ 'size' => (int)$row['size'],
'component' => strtolower($row['componenttype']),
- 'classification' => (int) $row['classification'],
- '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
+ 'classification' => (int)$row['classification'],
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
];
}
$stmt->closeCursor();
@@ -1073,7 +1206,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'size' => (int)$row['size'],
'component' => strtolower($row['componenttype']),
'classification' => (int)$row['classification'],
- '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
];
}
$stmt->closeCursor();
@@ -1104,7 +1237,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return $this->cachedObjects[$key];
}
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
+ $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
->from('calendarobjects')
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
@@ -1126,6 +1259,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return [
'id' => $row['id'],
'uri' => $row['uri'],
+ 'uid' => $row['uid'],
'lastmodified' => $row['lastmodified'],
'etag' => '"' . $row['etag'] . '"',
'calendarid' => $row['calendarid'],
@@ -1133,7 +1267,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'calendardata' => $this->readBlob($row['calendardata']),
'component' => strtolower($row['componenttype']),
'classification' => (int)$row['classification'],
- '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int) $row['deleted_at'],
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
];
}
@@ -1222,7 +1356,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
->andWhere($qb->expr()->isNull('deleted_at'));
$result = $qb->executeQuery();
- $count = (int) $result->fetchOne();
+ $count = (int)$result->fetchOne();
$result->closeCursor();
if ($count !== 0) {
@@ -1310,15 +1444,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
$query = $this->db->getQueryBuilder();
$query->update('calendarobjects')
- ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
- ->set('lastmodified', $query->createNamedParameter(time()))
- ->set('etag', $query->createNamedParameter($extraData['etag']))
- ->set('size', $query->createNamedParameter($extraData['size']))
- ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
- ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
- ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
- ->set('classification', $query->createNamedParameter($extraData['classification']))
- ->set('uid', $query->createNamedParameter($extraData['uid']))
+ ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
+ ->set('lastmodified', $query->createNamedParameter(time()))
+ ->set('etag', $query->createNamedParameter($extraData['etag']))
+ ->set('size', $query->createNamedParameter($extraData['size']))
+ ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
+ ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
+ ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
+ ->set('classification', $query->createNamedParameter($extraData['classification']))
+ ->set('uid', $query->createNamedParameter($extraData['uid']))
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
@@ -1348,37 +1482,40 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* Moves a calendar object from calendar to calendar.
*
- * @param int $sourceCalendarId
+ * @param string $sourcePrincipalUri
+ * @param int $sourceObjectId
+ * @param string $targetPrincipalUri
* @param int $targetCalendarId
- * @param int $objectId
- * @param string $oldPrincipalUri
- * @param string $newPrincipalUri
+ * @param string $tragetObjectUri
* @param int $calendarType
* @return bool
* @throws Exception
*/
- public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
+ public function moveCalendarObject(string $sourcePrincipalUri, int $sourceObjectId, string $targetPrincipalUri, int $targetCalendarId, string $tragetObjectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
$this->cachedObjects = [];
- return $this->atomic(function () use ($sourceCalendarId, $targetCalendarId, $objectId, $oldPrincipalUri, $newPrincipalUri, $calendarType) {
- $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId);
+ return $this->atomic(function () use ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
+ $object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId);
if (empty($object)) {
return false;
}
+ $sourceCalendarId = $object['calendarid'];
+ $sourceObjectUri = $object['uri'];
+
$query = $this->db->getQueryBuilder();
$query->update('calendarobjects')
->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
- ->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
+ ->set('uri', $query->createNamedParameter($tragetObjectUri, IQueryBuilder::PARAM_STR))
+ ->where($query->expr()->eq('id', $query->createNamedParameter($sourceObjectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
->executeStatement();
- $this->purgeProperties($sourceCalendarId, $objectId);
- $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
+ $this->purgeProperties($sourceCalendarId, $sourceObjectId);
+ $this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType);
- $this->addChanges($sourceCalendarId, [$object['uri']], 3, $calendarType);
- $this->addChanges($targetCalendarId, [$object['uri']], 1, $calendarType);
+ $this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType);
+ $this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType);
- $object = $this->getCalendarObjectById($newPrincipalUri, $objectId);
+ $object = $this->getCalendarObjectById($targetPrincipalUri, $sourceObjectId);
// Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
if (empty($object)) {
return false;
@@ -1400,25 +1537,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}, $this->db);
}
-
- /**
- * @param int $calendarObjectId
- * @param int $classification
- */
- public function setClassification($calendarObjectId, $classification) {
- $this->cachedObjects = [];
- if (!in_array($classification, [
- self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
- ])) {
- throw new \InvalidArgumentException();
- }
- $query = $this->db->getQueryBuilder();
- $query->update('calendarobjects')
- ->set('classification', $query->createNamedParameter($classification))
- ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
- ->executeStatement();
- }
-
/**
* Deletes an existing calendar object.
*
@@ -1432,7 +1550,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/
public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
$this->cachedObjects = [];
- $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently) {
+ $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
$data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
if ($data === null) {
@@ -1446,6 +1564,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->purgeProperties($calendarId, $data['id']);
+ $this->purgeObjectInvitations($data['uid']);
+
if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
$calendarRow = $this->getCalendarById($calendarId);
$shares = $this->getShares($calendarId);
@@ -1461,13 +1581,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
if (!empty($pathInfo['extension'])) {
// Append a suffix to "free" the old URI for recreation
$newUri = sprintf(
- "%s-deleted.%s",
+ '%s-deleted.%s',
$pathInfo['filename'],
$pathInfo['extension']
);
} else {
$newUri = sprintf(
- "%s-deleted",
+ '%s-deleted',
$pathInfo['filename']
);
}
@@ -1514,9 +1634,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/
public function restoreCalendarObject(array $objectData): void {
$this->cachedObjects = [];
- $this->atomic(function () use ($objectData) {
- $id = (int) $objectData['id'];
- $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
+ $this->atomic(function () use ($objectData): void {
+ $id = (int)$objectData['id'];
+ $restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']);
$targetObject = $this->getCalendarObject(
$objectData['calendarid'],
$restoreUri
@@ -1545,17 +1665,17 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
// Welp, this should possibly not have happened, but let's ignore
return;
}
- $this->addChanges($row['calendarid'], [$row['uri']], 1, (int) $row['calendartype']);
+ $this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']);
- $calendarRow = $this->getCalendarById((int) $row['calendarid']);
+ $calendarRow = $this->getCalendarById((int)$row['calendarid']);
if ($calendarRow === null) {
throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
}
$this->dispatcher->dispatchTyped(
new CalendarObjectRestoredEvent(
- (int) $objectData['calendarid'],
+ (int)$objectData['calendarid'],
$calendarRow,
- $this->getShares((int) $row['calendarid']),
+ $this->getShares((int)$row['calendarid']),
$row
)
);
@@ -1642,7 +1762,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
+ $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
->from('calendarobjects')
->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
@@ -1674,13 +1794,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
try {
$matches = $this->validateFilterForObject($row, $filters);
} catch (ParseException $ex) {
- $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [
+ $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
'app' => 'dav',
'exception' => $ex,
]);
continue;
} catch (InvalidDataException $ex) {
- $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'], [
+ $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
+ 'app' => 'dav',
+ 'exception' => $ex,
+ ]);
+ continue;
+ } catch (MaxInstancesExceededException $ex) {
+ $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
'app' => 'dav',
'exception' => $ex,
]);
@@ -1803,7 +1929,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->andWhere($compExpr)
->andWhere($propParamExpr)
->andWhere($query->expr()->iLike('i.value',
- $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
+ $query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%')))
->andWhere($query->expr()->isNull('deleted_at'));
if ($offset) {
@@ -1845,7 +1971,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
array $searchProperties,
array $options,
$limit,
- $offset
+ $offset,
) {
$outerQuery = $this->db->getQueryBuilder();
$innerQuery = $this->db->getQueryBuilder();
@@ -1874,18 +2000,18 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
if (!empty($searchProperties)) {
- $or = $innerQuery->expr()->orX();
+ $or = [];
foreach ($searchProperties as $searchProperty) {
- $or->add($innerQuery->expr()->eq('op.name',
- $outerQuery->createNamedParameter($searchProperty)));
+ $or[] = $innerQuery->expr()->eq('op.name',
+ $outerQuery->createNamedParameter($searchProperty));
}
- $innerQuery->andWhere($or);
+ $innerQuery->andWhere($innerQuery->expr()->orX(...$or));
}
if ($pattern !== '') {
$innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
- $outerQuery->createNamedParameter('%' .
- $this->db->escapeLikeParameter($pattern) . '%')));
+ $outerQuery->createNamedParameter('%'
+ . $this->db->escapeLikeParameter($pattern) . '%')));
}
$start = null;
@@ -1923,12 +2049,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
if (!empty($options['types'])) {
- $or = $outerQuery->expr()->orX();
+ $or = [];
foreach ($options['types'] as $type) {
- $or->add($outerQuery->expr()->eq('componenttype',
- $outerQuery->createNamedParameter($type)));
+ $or[] = $outerQuery->expr()->eq('componenttype',
+ $outerQuery->createNamedParameter($type));
}
- $outerQuery->andWhere($or);
+ $outerQuery->andWhere($outerQuery->expr()->orX(...$or));
}
$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
@@ -2021,7 +2147,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return $calendarObjects;
}
- private function searchCalendarObjects(IQueryBuilder $query, DateTimeInterface|null $start, DateTimeInterface|null $end): array {
+ private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface $start, ?DateTimeInterface $end): array {
$calendarObjects = [];
$filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
@@ -2034,24 +2160,32 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
continue;
}
- $isValid = $this->validateFilterForObject($row, [
- 'name' => 'VCALENDAR',
- 'comp-filters' => [
- [
- 'name' => 'VEVENT',
- 'comp-filters' => [],
- 'prop-filters' => [],
- 'is-not-defined' => false,
- 'time-range' => [
- 'start' => $start,
- 'end' => $end,
+ try {
+ $isValid = $this->validateFilterForObject($row, [
+ 'name' => 'VCALENDAR',
+ 'comp-filters' => [
+ [
+ 'name' => 'VEVENT',
+ 'comp-filters' => [],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => [
+ 'start' => $start,
+ 'end' => $end,
+ ],
],
],
- ],
- 'prop-filters' => [],
- 'is-not-defined' => false,
- 'time-range' => null,
- ]);
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ ]);
+ } catch (MaxInstancesExceededException $ex) {
+ $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
+ 'app' => 'dav',
+ 'exception' => $ex,
+ ]);
+ continue;
+ }
if (is_resource($row['calendardata'])) {
// Put the stream back to the beginning so it can be read another time
@@ -2143,22 +2277,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
array $componentTypes,
array $searchProperties,
array $searchParameters,
- array $options = []
+ array $options = [],
): array {
return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
$escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
$calendarObjectIdQuery = $this->db->getQueryBuilder();
- $calendarOr = $calendarObjectIdQuery->expr()->orX();
- $searchOr = $calendarObjectIdQuery->expr()->orX();
+ $calendarOr = [];
+ $searchOr = [];
// Fetch calendars and subscription
$calendars = $this->getCalendarsForUser($principalUri);
$subscriptions = $this->getSubscriptionsForUser($principalUri);
foreach ($calendars as $calendar) {
- $calendarAnd = $calendarObjectIdQuery->expr()->andX();
- $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])));
- $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
+ $calendarAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])),
+ $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)),
+ );
// If it's shared, limit search to public events
if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
@@ -2166,12 +2301,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}
- $calendarOr->add($calendarAnd);
+ $calendarOr[] = $calendarAnd;
}
foreach ($subscriptions as $subscription) {
- $subscriptionAnd = $calendarObjectIdQuery->expr()->andX();
- $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])));
- $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
+ $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])),
+ $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)),
+ );
// If it's shared, limit search to public events
if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
@@ -2179,28 +2315,30 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}
- $calendarOr->add($subscriptionAnd);
+ $calendarOr[] = $subscriptionAnd;
}
foreach ($searchProperties as $property) {
- $propertyAnd = $calendarObjectIdQuery->expr()->andX();
- $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
- $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter'));
+ $propertyAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
+ $calendarObjectIdQuery->expr()->isNull('cob.parameter'),
+ );
- $searchOr->add($propertyAnd);
+ $searchOr[] = $propertyAnd;
}
foreach ($searchParameters as $property => $parameter) {
- $parameterAnd = $calendarObjectIdQuery->expr()->andX();
- $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)));
- $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)));
+ $parameterAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
+ $calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)),
+ );
- $searchOr->add($parameterAnd);
+ $searchOr[] = $parameterAnd;
}
- if ($calendarOr->count() === 0) {
+ if (empty($calendarOr)) {
return [];
}
- if ($searchOr->count() === 0) {
+ if (empty($searchOr)) {
return [];
}
@@ -2208,8 +2346,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->from($this->dbObjectPropertiesTable, 'cob')
->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
- ->andWhere($calendarOr)
- ->andWhere($searchOr)
+ ->andWhere($calendarObjectIdQuery->expr()->orX(...$calendarOr))
+ ->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr))
->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
if ($pattern !== '') {
@@ -2244,7 +2382,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$result = $calendarObjectIdQuery->executeQuery();
$matches = [];
while (($row = $result->fetch()) !== false) {
- $matches[] = (int) $row['objectid'];
+ $matches[] = (int)$row['objectid'];
}
$result->closeCursor();
@@ -2331,7 +2469,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'calendardata' => $this->readBlob($row['calendardata']),
'component' => strtolower($row['componenttype']),
'classification' => (int)$row['classification'],
- 'deleted_at' => isset($row['deleted_at']) ? ((int) $row['deleted_at']) : null,
+ 'deleted_at' => isset($row['deleted_at']) ? ((int)$row['deleted_at']) : null,
];
}
@@ -2405,74 +2543,57 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
);
$stmt = $qb->executeQuery();
$currentToken = $stmt->fetchOne();
+ $initialSync = !is_numeric($syncToken);
if ($currentToken === false) {
return null;
}
- $result = [
- 'syncToken' => $currentToken,
- 'added' => [],
- 'modified' => [],
- 'deleted' => [],
- ];
-
- if ($syncToken) {
- $qb = $this->db->getQueryBuilder();
-
- $qb->select('uri', 'operation')
- ->from('calendarchanges')
- ->where(
- $qb->expr()->andX(
- $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)),
- $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)),
- $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
- $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
- )
- )->orderBy('synctoken');
- if (is_int($limit) && $limit > 0) {
- $qb->setMaxResults($limit);
- }
-
- // Fetching all changes
- $stmt = $qb->executeQuery();
- $changes = [];
-
- // This loop ensures that any duplicates are overwritten, only the
- // last change on a node is relevant.
- while ($row = $stmt->fetch()) {
- $changes[$row['uri']] = $row['operation'];
- }
- $stmt->closeCursor();
-
- foreach ($changes as $uri => $operation) {
- switch ($operation) {
- case 1:
- $result['added'][] = $uri;
- break;
- case 2:
- $result['modified'][] = $uri;
- break;
- case 3:
- $result['deleted'][] = $uri;
- break;
- }
- }
- } else {
- // No synctoken supplied, this is the initial sync.
+ // evaluate if this is a initial sync and construct appropriate command
+ if ($initialSync) {
$qb = $this->db->getQueryBuilder();
$qb->select('uri')
->from('calendarobjects')
- ->where(
- $qb->expr()->andX(
- $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
- $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType))
- )
- );
- $stmt = $qb->executeQuery();
+ ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
+ ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
+ ->andWhere($qb->expr()->isNull('deleted_at'));
+ } else {
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('uri', $qb->func()->max('operation'))
+ ->from('calendarchanges')
+ ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
+ ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
+ ->andWhere($qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)))
+ ->andWhere($qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)))
+ ->groupBy('uri');
+ }
+ // evaluate if limit exists
+ if (is_numeric($limit)) {
+ $qb->setMaxResults($limit);
+ }
+ // execute command
+ $stmt = $qb->executeQuery();
+ // build results
+ $result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve results
+ if ($initialSync) {
$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
- $stmt->closeCursor();
+ } else {
+ // \PDO::FETCH_NUM is needed due to the inconsistent field names
+ // produced by doctrine for MAX() with different databases
+ while ($entry = $stmt->fetch(\PDO::FETCH_NUM)) {
+ // assign uri (column 0) to appropriate mutation based on operation (column 1)
+ // forced (int) is needed as doctrine with OCI returns the operation field as string not integer
+ match ((int)$entry[1]) {
+ 1 => $result['added'][] = $entry[0],
+ 2 => $result['modified'][] = $entry[0],
+ 3 => $result['deleted'][] = $entry[0],
+ default => $this->logger->debug('Unknown calendar change operation detected')
+ };
+ }
}
+ $stmt->closeCursor();
+
return $result;
}, $this->db);
}
@@ -2535,7 +2656,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'lastmodified' => $row['lastmodified'],
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
- '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
+ '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
];
$subscriptions[] = $this->rowToSubscription($row, $subscription);
@@ -2657,7 +2778,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteSubscription($subscriptionId) {
- $this->atomic(function () use ($subscriptionId) {
+ $this->atomic(function () use ($subscriptionId): void {
$subscriptionRow = $this->getSubscriptionById($subscriptionId);
$query = $this->db->getQueryBuilder();
@@ -2740,9 +2861,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
public function getSchedulingObjects($principalUri) {
$query = $this->db->getQueryBuilder();
$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
- ->from('schedulingobjects')
- ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
- ->executeQuery();
+ ->from('schedulingobjects')
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->executeQuery();
$results = [];
while (($row = $stmt->fetch()) !== false) {
@@ -2770,9 +2891,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->cachedObjects = [];
$query = $this->db->getQueryBuilder();
$query->delete('schedulingobjects')
- ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
- ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
- ->executeStatement();
+ ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+ ->executeStatement();
}
/**
@@ -2790,7 +2911,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->setMaxResults($limit);
$result = $query->executeQuery();
$count = $result->rowCount();
- if($count === 0) {
+ if ($count === 0) {
return;
}
$ids = array_map(static function (array $id) {
@@ -2802,12 +2923,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$deleteQuery = $this->db->getQueryBuilder();
$deleteQuery->delete('schedulingobjects')
->where($deleteQuery->expr()->in('id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY));
- foreach(array_chunk($ids, 1000) as $chunk) {
+ foreach (array_chunk($ids, 1000) as $chunk) {
$deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
$numDeleted += $deleteQuery->executeStatement();
}
- if($numDeleted === $limit) {
+ if ($numDeleted === $limit) {
$this->logger->info("Deleted $limit scheduling objects, continuing with next batch");
$this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit);
}
@@ -2849,7 +2970,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->cachedObjects = [];
$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
- $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table) {
+ $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
$query = $this->db->getQueryBuilder();
$query->select('synctoken')
->from($table)
@@ -2866,7 +2987,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'calendarid' => $query->createNamedParameter($calendarId),
'operation' => $query->createNamedParameter($operation),
'calendartype' => $query->createNamedParameter($calendarType),
- 'created_at' => time(),
+ 'created_at' => $query->createNamedParameter(time()),
]);
foreach ($objectUris as $uri) {
$query->setParameter('uri', $uri);
@@ -2884,7 +3005,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
$this->cachedObjects = [];
- $this->atomic(function () use ($calendarId, $calendarType) {
+ $this->atomic(function () use ($calendarId, $calendarType): void {
$qbAdded = $this->db->getQueryBuilder();
$qbAdded->select('uri')
->from('calendarobjects')
@@ -2915,7 +3036,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
);
$resultDeleted = $qbDeleted->executeQuery();
$deletedUris = array_map(function (string $uri) {
- return str_replace("-deleted.ics", ".ics", $uri);
+ return str_replace('-deleted.ics', '.ics', $uri);
}, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
$resultDeleted->closeCursor();
$this->addChanges($calendarId, $deletedUris, 3, $calendarType);
@@ -2987,7 +3108,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$lastOccurrence = $firstOccurrence;
}
} else {
- $it = new EventIterator($vEvents);
+ try {
+ $it = new EventIterator($vEvents);
+ } catch (NoInstancesException $e) {
+ $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
+ 'app' => 'dav',
+ 'exception' => $e,
+ ]);
+ throw new Forbidden($e->getMessage());
+ }
$maxDate = new DateTime(self::MAX_DATE);
$firstOccurrence = $it->getDtStart()->getTimestamp();
if ($it->isInfinite()) {
@@ -3042,7 +3171,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param list<string> $remove
*/
public function updateShares(IShareable $shareable, array $add, array $remove): void {
- $this->atomic(function () use ($shareable, $add, $remove) {
+ $this->atomic(function () use ($shareable, $add, $remove): void {
$calendarId = $shareable->getResourceId();
$calendarRow = $this->getCalendarById($calendarId);
if ($calendarRow === null) {
@@ -3069,7 +3198,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* @param boolean $value
- * @param \OCA\DAV\CalDAV\Calendar $calendar
+ * @param Calendar $calendar
* @return string|null
*/
public function setPublishStatus($value, $calendar) {
@@ -3104,7 +3233,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * @param \OCA\DAV\CalDAV\Calendar $calendar
+ * @param Calendar $calendar
* @return mixed
*/
public function getPublishStatus($calendar) {
@@ -3140,7 +3269,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*/
public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
$this->cachedObjects = [];
- $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType) {
+ $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void {
$objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
try {
@@ -3181,7 +3310,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$query->setParameter('name', $property->name);
$query->setParameter('parameter', null);
- $query->setParameter('value', $value);
+ $query->setParameter('value', mb_strcut($value, 0, 254));
$query->executeStatement();
}
@@ -3212,7 +3341,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* deletes all birthday calendars
*/
public function deleteAllBirthdayCalendars() {
- $this->atomic(function () {
+ $this->atomic(function (): void {
$query = $this->db->getQueryBuilder();
$result = $query->select(['id'])->from('calendars')
->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
@@ -3232,7 +3361,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param $subscriptionId
*/
public function purgeAllCachedEventsForSubscription($subscriptionId) {
- $this->atomic(function () use ($subscriptionId) {
+ $this->atomic(function () use ($subscriptionId): void {
$query = $this->db->getQueryBuilder();
$query->select('uri')
->from('calendarobjects')
@@ -3269,6 +3398,45 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * @param int $subscriptionId
+ * @param array<int> $calendarObjectIds
+ * @param array<string> $calendarObjectUris
+ */
+ public function purgeCachedEventsForSubscription(int $subscriptionId, array $calendarObjectIds, array $calendarObjectUris): void {
+ if (empty($calendarObjectUris)) {
+ return;
+ }
+
+ $this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void {
+ foreach (array_chunk($calendarObjectIds, 1000) as $chunk) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete($this->dbObjectPropertiesTable)
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
+ ->executeStatement();
+
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
+ ->executeStatement();
+ }
+
+ foreach (array_chunk($calendarObjectUris, 1000) as $chunk) {
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarchanges')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->andWhere($query->expr()->in('uri', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
+ ->executeStatement();
+ }
+ $this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
+ }, $this->db);
+ }
+
+ /**
* Move a calendar from one user to another
*
* @param string $uriName
@@ -3351,7 +3519,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->from('calendarchanges');
$result = $query->executeQuery();
- $maxId = (int) $result->fetchOne();
+ $maxId = (int)$result->fetchOne();
$result->closeCursor();
if (!$maxId || $maxId < $keep) {
return 0;
@@ -3455,4 +3623,68 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
return $subscription;
}
+
+ /**
+ * delete all invitations from a given calendar
+ *
+ * @since 31.0.0
+ *
+ * @param int $calendarId
+ *
+ * @return void
+ */
+ protected function purgeCalendarInvitations(int $calendarId): void {
+ // select all calendar object uid's
+ $cmd = $this->db->getQueryBuilder();
+ $cmd->select('uid')
+ ->from($this->dbObjectsTable)
+ ->where($cmd->expr()->eq('calendarid', $cmd->createNamedParameter($calendarId)));
+ $allIds = $cmd->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
+ // delete all links that match object uid's
+ $cmd = $this->db->getQueryBuilder();
+ $cmd->delete($this->dbObjectInvitationsTable)
+ ->where($cmd->expr()->in('uid', $cmd->createParameter('uids'), IQueryBuilder::PARAM_STR_ARRAY));
+ foreach (array_chunk($allIds, 1000) as $chunkIds) {
+ $cmd->setParameter('uids', $chunkIds, IQueryBuilder::PARAM_STR_ARRAY);
+ $cmd->executeStatement();
+ }
+ }
+
+ /**
+ * Delete all invitations from a given calendar event
+ *
+ * @since 31.0.0
+ *
+ * @param string $eventId UID of the event
+ *
+ * @return void
+ */
+ protected function purgeObjectInvitations(string $eventId): void {
+ $cmd = $this->db->getQueryBuilder();
+ $cmd->delete($this->dbObjectInvitationsTable)
+ ->where($cmd->expr()->eq('uid', $cmd->createNamedParameter($eventId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
+ $cmd->executeStatement();
+ }
+
+ public function unshare(IShareable $shareable, string $principal): void {
+ $this->atomic(function () use ($shareable, $principal): void {
+ $calendarData = $this->getCalendarById($shareable->getResourceId());
+ if ($calendarData === null) {
+ throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $shareable->getResourceId());
+ }
+
+ $oldShares = $this->getShares($shareable->getResourceId());
+ $unshare = $this->calendarSharingBackend->unshare($shareable, $principal);
+
+ if ($unshare) {
+ $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent(
+ $shareable->getResourceId(),
+ $calendarData,
+ $oldShares,
+ [],
+ [$principal]
+ ));
+ }
+ }, $this->db);
+ }
}
diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php
index eaf8d227a6f..dd3a4cf3f69 100644
--- a/apps/dav/lib/CalDAV/Calendar.php
+++ b/apps/dav/lib/CalDAV/Calendar.php
@@ -31,12 +31,16 @@ use Sabre\DAV\PropPatch;
* @property CalDavBackend $caldavBackend
*/
class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget {
- private IConfig $config;
protected IL10N $l10n;
private bool $useTrashbin = true;
- private LoggerInterface $logger;
- public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
+ public function __construct(
+ BackendInterface $caldavBackend,
+ $calendarInfo,
+ IL10N $l10n,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
// Convert deletion date to ISO8601 string
if (isset($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
$calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] = (new DateTimeImmutable())
@@ -46,17 +50,14 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
parent::__construct($caldavBackend, $calendarInfo);
- if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
+ if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI && strcasecmp($this->calendarInfo['{DAV:}displayname'], 'Contact birthdays') === 0) {
$this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays');
}
- if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI &&
- $this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
+ if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI
+ && $this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
$this->calendarInfo['{DAV:}displayname'] = $l10n->t('Personal');
}
-
- $this->config = $config;
$this->l10n = $l10n;
- $this->logger = $logger;
}
/**
@@ -189,8 +190,8 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
$acl = $this->caldavBackend->applyShareAcl($this->getResourceId(), $acl);
$allowedPrincipals = [
$this->getOwner(),
- $this->getOwner(). '/calendar-proxy-read',
- $this->getOwner(). '/calendar-proxy-write',
+ $this->getOwner() . '/calendar-proxy-read',
+ $this->getOwner() . '/calendar-proxy-write',
parent::getOwner(),
'principals/system/public'
];
@@ -213,12 +214,8 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
}
public function delete() {
- if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) &&
- $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) {
- $principal = 'principal:' . parent::getOwner();
- $this->caldavBackend->updateShares($this, [], [
- $principal
- ]);
+ if ($this->isShared()) {
+ $this->caldavBackend->unshare($this, 'principal:' . $this->getPrincipalURI());
return;
}
@@ -376,7 +373,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
* @inheritDoc
*/
public function restore(): void {
- $this->caldavBackend->restoreCalendar((int) $this->calendarInfo['id']);
+ $this->caldavBackend->restoreCalendar((int)$this->calendarInfo['id']);
}
public function disableTrashbin(): void {
@@ -390,9 +387,14 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
if (!($sourceNode instanceof CalendarObject)) {
return false;
}
-
try {
- return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getOwner(), $this->getOwner());
+ return $this->caldavBackend->moveCalendarObject(
+ $sourceNode->getOwner(),
+ $sourceNode->getId(),
+ $this->getOwner(),
+ $this->getResourceId(),
+ $targetName,
+ );
} catch (Exception $e) {
$this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
return false;
diff --git a/apps/dav/lib/CalDAV/CalendarHome.php b/apps/dav/lib/CalDAV/CalendarHome.php
index d01c2affbc8..89b78ba9007 100644
--- a/apps/dav/lib/CalDAV/CalendarHome.php
+++ b/apps/dav/lib/CalDAV/CalendarHome.php
@@ -11,6 +11,10 @@ use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
+use OCP\App\IAppManager;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\CalDAV\Backend\NotificationSupport;
@@ -25,33 +29,29 @@ use Sabre\DAV\MkCol;
class CalendarHome extends \Sabre\CalDAV\CalendarHome {
- /** @var \OCP\IL10N */
+ /** @var IL10N */
private $l10n;
- /** @var \OCP\IConfig */
+ /** @var IConfig */
private $config;
/** @var PluginManager */
private $pluginManager;
-
- /** @var LoggerInterface */
- private $logger;
private ?array $cachedChildren = null;
public function __construct(
BackendInterface $caldavBackend,
array $principalInfo,
- LoggerInterface $logger,
- private bool $returnCachedSubscriptions
+ private LoggerInterface $logger,
+ private bool $returnCachedSubscriptions,
) {
parent::__construct($caldavBackend, $principalInfo);
$this->l10n = \OC::$server->getL10N('dav');
- $this->config = \OC::$server->getConfig();
+ $this->config = Server::get(IConfig::class);
$this->pluginManager = new PluginManager(
\OC::$server,
- \OC::$server->getAppManager()
+ Server::get(IAppManager::class)
);
- $this->logger = $logger;
}
/**
@@ -149,9 +149,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
// Calendar - this covers all "regular" calendars, but not shared
// only check if the method is available
- if($this->caldavBackend instanceof CalDavBackend) {
+ if ($this->caldavBackend instanceof CalDavBackend) {
$calendar = $this->caldavBackend->getCalendarByUri($this->principalInfo['uri'], $name);
- if(!empty($calendar)) {
+ if (!empty($calendar)) {
return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
}
}
diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php
index cac9bad0d89..5f912da732e 100644
--- a/apps/dav/lib/CalDAV/CalendarImpl.php
+++ b/apps/dav/lib/CalDAV/CalendarImpl.php
@@ -8,9 +8,15 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CalDAV;
+use Generator;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
+use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\Exceptions\CalendarException;
+use OCP\Calendar\ICalendarExport;
+use OCP\Calendar\ICalendarIsEnabled;
+use OCP\Calendar\ICalendarIsShared;
+use OCP\Calendar\ICalendarIsWritable;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IHandleImipMessage;
use OCP\Constants;
@@ -24,18 +30,13 @@ use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
-class CalendarImpl implements ICreateFromString, IHandleImipMessage {
- private CalDavBackend $backend;
- private Calendar $calendar;
- /** @var array<string, mixed> */
- private array $calendarInfo;
-
- public function __construct(Calendar $calendar,
- array $calendarInfo,
- CalDavBackend $backend) {
- $this->calendar = $calendar;
- $this->calendarInfo = $calendarInfo;
- $this->backend = $backend;
+class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarExport, ICalendarIsEnabled {
+ public function __construct(
+ private Calendar $calendar,
+ /** @var array<string, mixed> */
+ private array $calendarInfo,
+ private CalDavBackend $backend,
+ ) {
}
/**
@@ -43,7 +44,7 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
* @since 13.0.0
*/
public function getKey(): string {
- return (string) $this->calendarInfo['id'];
+ return (string)$this->calendarInfo['id'];
}
/**
@@ -84,7 +85,7 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
/** @var VCalendar $vobj */
$vobj = Reader::read($timezoneProp);
$components = $vobj->getComponents();
- if(empty($components)) {
+ if (empty($components)) {
return null;
}
/** @var VTimeZone $vtimezone */
@@ -92,16 +93,6 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
return $vtimezone;
}
- /**
- * @param string $pattern which should match within the $searchProperties
- * @param array $searchProperties defines the properties within the query pattern should match
- * @param array $options - optional parameters:
- * ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
- * @param int|null $limit - limit number of search results
- * @param int|null $offset - offset for paging of search results
- * @return array an array of events/journals/todos which are arrays of key-value-pairs
- * @since 13.0.0
- */
public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array {
return $this->backend->search($this->calendarInfo, $pattern,
$searchProperties, $options, $limit, $offset);
@@ -115,6 +106,10 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
$permissions = $this->calendar->getACL();
$result = 0;
foreach ($permissions as $permission) {
+ if ($this->calendarInfo['principaluri'] !== $permission['principal']) {
+ continue;
+ }
+
switch ($permission['privilege']) {
case '{DAV:}read':
$result |= Constants::PERMISSION_READ;
@@ -133,6 +128,20 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
}
/**
+ * @since 32.0.0
+ */
+ public function isEnabled(): bool {
+ return $this->calendarInfo['{http://owncloud.org/ns}calendar-enabled'] ?? true;
+ }
+
+ /**
+ * @since 31.0.0
+ */
+ public function isWritable(): bool {
+ return $this->calendar->canWrite();
+ }
+
+ /**
* @since 26.0.0
*/
public function isDeleted(): bool {
@@ -140,19 +149,22 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
}
/**
- * Create a new calendar event for this calendar
- * by way of an ICS string
- *
- * @param string $name the file name - needs to contain the .ics ending
- * @param string $calendarData a string containing a valid VEVENT ics
- *
- * @throws CalendarException
+ * @since 31.0.0
*/
- public function createFromString(string $name, string $calendarData): void {
- $server = new InvitationResponseServer(false);
+ public function isShared(): bool {
+ return $this->calendar->isShared();
+ }
+ /**
+ * @throws CalendarException
+ */
+ private function createFromStringInServer(
+ string $name,
+ string $calendarData,
+ \OCA\DAV\Connector\Sabre\Server $server,
+ ): void {
/** @var CustomPrincipalPlugin $plugin */
- $plugin = $server->getServer()->getPlugin('auth');
+ $plugin = $server->getPlugin('auth');
// we're working around the previous implementation
// that only allowed the public system principal to be used
// so set the custom principal here
@@ -168,14 +180,14 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
// Force calendar change URI
/** @var Schedule\Plugin $schedulingPlugin */
- $schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule');
+ $schedulingPlugin = $server->getPlugin('caldav-schedule');
$schedulingPlugin->setPathOfCalendarObjectChange($fullCalendarFilename);
$stream = fopen('php://memory', 'rb+');
fwrite($stream, $calendarData);
rewind($stream);
try {
- $server->getServer()->createFile($fullCalendarFilename, $stream);
+ $server->createFile($fullCalendarFilename, $stream);
} catch (Conflict $e) {
throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e);
} finally {
@@ -183,6 +195,16 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
}
}
+ public function createFromString(string $name, string $calendarData): void {
+ $server = new EmbeddedCalDavServer(false);
+ $this->createFromStringInServer($name, $calendarData, $server->getServer());
+ }
+
+ public function createFromStringMinimal(string $name, string $calendarData): void {
+ $server = new InvitationResponseServer(false);
+ $this->createFromStringInServer($name, $calendarData, $server->getServer());
+ }
+
/**
* @throws CalendarException
*/
@@ -220,7 +242,10 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
$attendee = $vEvent->{'ATTENDEE'}->getValue();
$iTipMessage->method = $vObject->{'METHOD'}->getValue();
- if ($iTipMessage->method === 'REPLY') {
+ if ($iTipMessage->method === 'REQUEST') {
+ $iTipMessage->sender = $organizer;
+ $iTipMessage->recipient = $attendee;
+ } elseif ($iTipMessage->method === 'REPLY') {
if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
$iTipMessage->recipient = $organizer;
} else {
@@ -241,4 +266,27 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
public function getInvitationResponseServer(): InvitationResponseServer {
return new InvitationResponseServer(false);
}
+
+ /**
+ * Export objects
+ *
+ * @since 32.0.0
+ *
+ * @return Generator<mixed, \Sabre\VObject\Component\VCalendar, mixed, mixed>
+ */
+ public function export(?CalendarExportOptions $options = null): Generator {
+ foreach (
+ $this->backend->exportCalendar(
+ $this->calendarInfo['id'],
+ $this->backend::CALENDAR_TYPE_CALENDAR,
+ $options
+ ) as $event
+ ) {
+ $vObject = Reader::read($event['calendardata']);
+ if ($vObject instanceof VCalendar) {
+ yield $vObject;
+ }
+ }
+ }
+
}
diff --git a/apps/dav/lib/CalDAV/CalendarManager.php b/apps/dav/lib/CalDAV/CalendarManager.php
index a415a830d2e..a2d2f1cda8a 100644
--- a/apps/dav/lib/CalDAV/CalendarManager.php
+++ b/apps/dav/lib/CalDAV/CalendarManager.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -12,18 +13,6 @@ use Psr\Log\LoggerInterface;
class CalendarManager {
- /** @var CalDavBackend */
- private $backend;
-
- /** @var IL10N */
- private $l10n;
-
- /** @var IConfig */
- private $config;
-
- /** @var LoggerInterface */
- private $logger;
-
/**
* CalendarManager constructor.
*
@@ -31,11 +20,12 @@ class CalendarManager {
* @param IL10N $l10n
* @param IConfig $config
*/
- public function __construct(CalDavBackend $backend, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
- $this->backend = $backend;
- $this->l10n = $l10n;
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private CalDavBackend $backend,
+ private IL10N $l10n,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/CalendarObject.php b/apps/dav/lib/CalDAV/CalendarObject.php
index f7af3ce8b4b..02178b4236f 100644
--- a/apps/dav/lib/CalDAV/CalendarObject.php
+++ b/apps/dav/lib/CalDAV/CalendarObject.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
@@ -13,9 +14,6 @@ use Sabre\VObject\Reader;
class CalendarObject extends \Sabre\CalDAV\CalendarObject {
- /** @var IL10N */
- protected $l10n;
-
/**
* CalendarObject constructor.
*
@@ -24,16 +22,17 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
* @param array $calendarInfo
* @param array $objectData
*/
- public function __construct(CalDavBackend $caldavBackend, IL10N $l10n,
+ public function __construct(
+ CalDavBackend $caldavBackend,
+ protected IL10N $l10n,
array $calendarInfo,
- array $objectData) {
+ array $objectData,
+ ) {
parent::__construct($caldavBackend, $calendarInfo, $objectData);
if ($this->isShared()) {
unset($this->objectData['size']);
}
-
- $this->l10n = $l10n;
}
/**
@@ -62,7 +61,7 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
}
public function getId(): int {
- return (int) $this->objectData['id'];
+ return (int)$this->objectData['id'];
}
protected function isShared() {
diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php
index 90605d4f76e..3cc4039ed36 100644
--- a/apps/dav/lib/CalDAV/CalendarProvider.php
+++ b/apps/dav/lib/CalDAV/CalendarProvider.php
@@ -8,6 +8,8 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CalDAV;
+use OCA\DAV\Db\Property;
+use OCA\DAV\Db\PropertyMapper;
use OCP\Calendar\ICalendarProvider;
use OCP\IConfig;
use OCP\IL10N;
@@ -15,39 +17,28 @@ use Psr\Log\LoggerInterface;
class CalendarProvider implements ICalendarProvider {
- /** @var CalDavBackend */
- private $calDavBackend;
-
- /** @var IL10N */
- private $l10n;
-
- /** @var IConfig */
- private $config;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(CalDavBackend $calDavBackend, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
- $this->calDavBackend = $calDavBackend;
- $this->l10n = $l10n;
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ private IL10N $l10n,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private PropertyMapper $propertyMapper,
+ ) {
}
public function getCalendars(string $principalUri, array $calendarUris = []): array {
- $calendarInfos = [];
- if (empty($calendarUris)) {
- $calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri);
- } else {
- foreach ($calendarUris as $calendarUri) {
- $calendarInfos[] = $this->calDavBackend->getCalendarByUri($principalUri, $calendarUri);
- }
- }
- $calendarInfos = array_filter($calendarInfos);
+ $calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri) ?? [];
+
+ if (!empty($calendarUris)) {
+ $calendarInfos = array_filter($calendarInfos, function ($calendar) use ($calendarUris) {
+ return in_array($calendar['uri'], $calendarUris);
+ });
+ }
$iCalendars = [];
foreach ($calendarInfos as $calendarInfo) {
+ $calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri']));
$calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$iCalendars[] = new CalendarImpl(
$calendar,
@@ -57,4 +48,23 @@ class CalendarProvider implements ICalendarProvider {
}
return $iCalendars;
}
+
+ public function getAdditionalProperties(string $principalUri, string $calendarUri): array {
+ $user = str_replace('principals/users/', '', $principalUri);
+ $path = 'calendars/' . $user . '/' . $calendarUri;
+
+ $properties = $this->propertyMapper->findPropertiesByPath($user, $path);
+
+ $list = [];
+ foreach ($properties as $property) {
+ if ($property instanceof Property) {
+ $list[$property->getPropertyname()] = match ($property->getPropertyname()) {
+ '{http://owncloud.org/ns}calendar-enabled' => (bool)$property->getPropertyvalue(),
+ default => $property->getPropertyvalue()
+ };
+ }
+ }
+
+ return $list;
+ }
}
diff --git a/apps/dav/lib/CalDAV/CalendarRoot.php b/apps/dav/lib/CalDAV/CalendarRoot.php
index 7e57b70c481..c0a313955bb 100644
--- a/apps/dav/lib/CalDAV/CalendarRoot.php
+++ b/apps/dav/lib/CalDAV/CalendarRoot.php
@@ -12,18 +12,15 @@ use Sabre\CalDAV\Backend;
use Sabre\DAVACL\PrincipalBackend;
class CalendarRoot extends \Sabre\CalDAV\CalendarRoot {
- private LoggerInterface $logger;
-
private array $returnCachedSubscriptions = [];
public function __construct(
PrincipalBackend\BackendInterface $principalBackend,
Backend\BackendInterface $caldavBackend,
$principalPrefix,
- LoggerInterface $logger
+ private LoggerInterface $logger,
) {
parent::__construct($principalBackend, $caldavBackend, $principalPrefix);
- $this->logger = $logger;
}
public function getChildForPrincipal(array $principal) {
@@ -36,8 +33,8 @@ class CalendarRoot extends \Sabre\CalDAV\CalendarRoot {
}
public function getName() {
- if ($this->principalPrefix === 'principals/calendar-resources' ||
- $this->principalPrefix === 'principals/calendar-rooms') {
+ if ($this->principalPrefix === 'principals/calendar-resources'
+ || $this->principalPrefix === 'principals/calendar-rooms') {
$parts = explode('/', $this->principalPrefix);
return $parts[1];
diff --git a/apps/dav/lib/CalDAV/DefaultCalendarValidator.php b/apps/dav/lib/CalDAV/DefaultCalendarValidator.php
new file mode 100644
index 00000000000..266e07ef255
--- /dev/null
+++ b/apps/dav/lib/CalDAV/DefaultCalendarValidator.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use Sabre\DAV\Exception as DavException;
+
+class DefaultCalendarValidator {
+ /**
+ * Check if a given Calendar node is suitable to be used as the default calendar for scheduling.
+ *
+ * @throws DavException If the calendar is not suitable to be used as the default calendar
+ */
+ public function validateScheduleDefaultCalendar(Calendar $calendar): void {
+ // Sanity checks for a calendar that should handle invitations
+ if ($calendar->isSubscription()
+ || !$calendar->canWrite()
+ || $calendar->isShared()
+ || $calendar->isDeleted()) {
+ throw new DavException('Calendar is a subscription, not writable, shared or deleted');
+ }
+
+ // Calendar must support VEVENTs
+ $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+ $calendarProperties = $calendar->getProperties([$sCCS]);
+ if (isset($calendarProperties[$sCCS])) {
+ $supportedComponents = $calendarProperties[$sCCS]->getValue();
+ } else {
+ $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
+ }
+ if (!in_array('VEVENT', $supportedComponents, true)) {
+ throw new DavException('Calendar does not support VEVENT components');
+ }
+ }
+}
diff --git a/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php
new file mode 100644
index 00000000000..21d8c06fa99
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php
@@ -0,0 +1,118 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use OCA\DAV\AppInfo\PluginManager;
+use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
+use OCA\DAV\CalDAV\Auth\PublicPrincipalPlugin;
+use OCA\DAV\CalDAV\Publishing\PublishPlugin;
+use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
+use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
+use OCA\DAV\Connector\Sabre\CachingTree;
+use OCA\DAV\Connector\Sabre\DavAclPlugin;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+use OCA\DAV\Connector\Sabre\LockPlugin;
+use OCA\DAV\Connector\Sabre\MaintenancePlugin;
+use OCA\DAV\Events\SabrePluginAuthInitEvent;
+use OCA\DAV\RootCollection;
+use OCA\Theming\ThemingDefaults;
+use OCP\App\IAppManager;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IAppConfig;
+use OCP\IConfig;
+use OCP\IURLGenerator;
+use OCP\L10N\IFactory as IL10NFactory;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+
+class EmbeddedCalDavServer {
+ private readonly \OCA\DAV\Connector\Sabre\Server $server;
+
+ public function __construct(bool $public = true) {
+ $baseUri = \OC::$WEBROOT . '/remote.php/dav/';
+ $logger = Server::get(LoggerInterface::class);
+ $dispatcher = Server::get(IEventDispatcher::class);
+ $appConfig = Server::get(IAppConfig::class);
+ $l10nFactory = Server::get(IL10NFactory::class);
+ $l10n = $l10nFactory->get('dav');
+
+ $root = new RootCollection();
+ $this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
+
+ // Add maintenance plugin
+ $this->server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), $l10n));
+
+ // Set URL explicitly due to reverse-proxy situations
+ $this->server->httpRequest->setUrl($baseUri);
+ $this->server->setBaseUri($baseUri);
+
+ $this->server->addPlugin(new BlockLegacyClientPlugin(
+ Server::get(IConfig::class),
+ Server::get(ThemingDefaults::class),
+ ));
+ $this->server->addPlugin(new AnonymousOptionsPlugin());
+
+ // allow custom principal uri option
+ if ($public) {
+ $this->server->addPlugin(new PublicPrincipalPlugin());
+ } else {
+ $this->server->addPlugin(new CustomPrincipalPlugin());
+ }
+
+ // allow setup of additional auth backends
+ $event = new SabrePluginAuthInitEvent($this->server);
+ $dispatcher->dispatchTyped($event);
+
+ $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger));
+ $this->server->addPlugin(new LockPlugin());
+ $this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
+
+ // acl
+ $acl = new DavAclPlugin();
+ $acl->principalCollectionSet = [
+ 'principals/users', 'principals/groups'
+ ];
+ $this->server->addPlugin($acl);
+
+ // calendar plugins
+ $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
+ $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
+ $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(Server::get(IConfig::class), Server::get(LoggerInterface::class), Server::get(DefaultCalendarValidator::class)));
+ $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
+ $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
+ //$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
+ $this->server->addPlugin(new PublishPlugin(
+ Server::get(IConfig::class),
+ Server::get(IURLGenerator::class)
+ ));
+ if ($appConfig->getValueString('dav', 'sendInvitations', 'yes') === 'yes') {
+ $this->server->addPlugin(Server::get(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class));
+ }
+
+ // wait with registering these until auth is handled and the filesystem is setup
+ $this->server->on('beforeMethod:*', function () use ($root): void {
+ // register plugins from apps
+ $pluginManager = new PluginManager(
+ \OC::$server,
+ Server::get(IAppManager::class)
+ );
+ foreach ($pluginManager->getAppPlugins() as $appPlugin) {
+ $this->server->addPlugin($appPlugin);
+ }
+ foreach ($pluginManager->getAppCollections() as $appCollection) {
+ $root->addChild($appCollection);
+ }
+ });
+ }
+
+ public function getServer(): \OCA\DAV\Connector\Sabre\Server {
+ return $this->server;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/EventComparisonService.php b/apps/dav/lib/CalDAV/EventComparisonService.php
index e3c7749a772..63395e7ce1c 100644
--- a/apps/dav/lib/CalDAV/EventComparisonService.php
+++ b/apps/dav/lib/CalDAV/EventComparisonService.php
@@ -37,14 +37,14 @@ class EventComparisonService {
*/
private function removeIfUnchanged(VEvent $filterEvent, array &$eventsToFilter): bool {
$filterEventData = [];
- foreach(self::EVENT_DIFF as $eventDiff) {
+ foreach (self::EVENT_DIFF as $eventDiff) {
$filterEventData[] = IMipService::readPropertyWithDefault($filterEvent, $eventDiff, '');
}
/** @var VEvent $component */
foreach ($eventsToFilter as $k => $eventToFilter) {
$eventToFilterData = [];
- foreach(self::EVENT_DIFF as $eventDiff) {
+ foreach (self::EVENT_DIFF as $eventDiff) {
$eventToFilterData[] = IMipService::readPropertyWithDefault($eventToFilter, $eventDiff, '');
}
// events are identical and can be removed
@@ -73,23 +73,23 @@ class EventComparisonService {
$newEventComponents = $new->getComponents();
foreach ($newEventComponents as $k => $event) {
- if(!$event instanceof VEvent) {
+ if (!$event instanceof VEvent) {
unset($newEventComponents[$k]);
}
}
- if(empty($old)) {
+ if (empty($old)) {
return ['old' => null, 'new' => $newEventComponents];
}
$oldEventComponents = $old->getComponents();
- if(is_array($oldEventComponents) && !empty($oldEventComponents)) {
+ if (is_array($oldEventComponents) && !empty($oldEventComponents)) {
foreach ($oldEventComponents as $k => $event) {
- if(!$event instanceof VEvent) {
+ if (!$event instanceof VEvent) {
unset($oldEventComponents[$k]);
continue;
}
- if($this->removeIfUnchanged($event, $newEventComponents)) {
+ if ($this->removeIfUnchanged($event, $newEventComponents)) {
unset($oldEventComponents[$k]);
}
}
diff --git a/apps/dav/lib/CalDAV/EventReader.php b/apps/dav/lib/CalDAV/EventReader.php
new file mode 100644
index 00000000000..ee2b8f33f9a
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReader.php
@@ -0,0 +1,771 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+use DateTimeImmutable;
+use DateTimeInterface;
+use DateTimeZone;
+use InvalidArgumentException;
+
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Reader;
+
+class EventReader {
+
+ protected VEvent $baseEvent;
+ protected DateTimeInterface $baseEventStartDate;
+ protected DateTimeZone $baseEventStartTimeZone;
+ protected DateTimeInterface $baseEventEndDate;
+ protected DateTimeZone $baseEventEndTimeZone;
+ protected bool $baseEventStartDateFloating = false;
+ protected bool $baseEventEndDateFloating = false;
+ protected int $baseEventDuration;
+
+ protected ?EventReaderRRule $rruleIterator = null;
+ protected ?EventReaderRDate $rdateIterator = null;
+ protected ?EventReaderRRule $eruleIterator = null;
+ protected ?EventReaderRDate $edateIterator = null;
+
+ protected array $recurrenceModified;
+ protected ?DateTimeInterface $recurrenceCurrentDate;
+
+ protected array $dayNamesMap = [
+ 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday', 'SU' => 'Sunday'
+ ];
+ protected array $monthNamesMap = [
+ 1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June',
+ 7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
+ ];
+ protected array $relativePositionNamesMap = [
+ 1 => 'First', 2 => 'Second', 3 => 'Third', 4 => 'Fourth', 5 => 'Fifth',
+ -1 => 'Last', -2 => 'Second Last', -3 => 'Third Last', -4 => 'Fourth Last', -5 => 'Fifth Last'
+ ];
+
+ /**
+ * Initilizes the Event Reader
+ *
+ * There is several ways to set up the iterator.
+ *
+ * 1. You can pass a VCALENDAR component (as object or string) and a UID.
+ * 2. You can pass an array of VEVENTs (all UIDS should match).
+ * 3. You can pass a single VEVENT component (as object or string).
+ *
+ * Only the second method is recommended. The other 1 and 3 will be removed
+ * at some point in the future.
+ *
+ * The $uid parameter is only required for the first method.
+ *
+ * @since 30.0.0
+ *
+ * @param VCalendar|VEvent|Array|String $input
+ * @param string|null $uid
+ * @param DateTimeZone|null $timeZone reference timezone for floating dates and times
+ */
+ public function __construct(VCalendar|VEvent|array|string $input, ?string $uid = null, ?DateTimeZone $timeZone = null) {
+
+ $timeZoneFactory = new TimeZoneFactory();
+
+ // evaluate if the input is a string and convert it to and vobject if required
+ if (is_string($input)) {
+ $input = Reader::read($input);
+ }
+ // evaluate if input is a single event vobject and convert it to a collection
+ if ($input instanceof VEvent) {
+ $events = [$input];
+ }
+ // evaluate if input is a calendar vobject
+ elseif ($input instanceof VCalendar) {
+ // Calendar + UID mode.
+ if ($uid === null) {
+ throw new InvalidArgumentException('The UID argument is required when a VCALENDAR object is used');
+ }
+ // extract events from calendar
+ $events = $input->getByUID($uid);
+ // evaluate if any event where found
+ if (count($events) === 0) {
+ throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: ' . $uid);
+ }
+ // extract calendar timezone
+ if (isset($input->VTIMEZONE) && isset($input->VTIMEZONE->TZID)) {
+ $calendarTimeZone = $timeZoneFactory->fromName($input->VTIMEZONE->TZID->getValue());
+ }
+ }
+ // evaluate if input is a collection of event vobjects
+ elseif (is_array($input)) {
+ $events = $input;
+ } else {
+ throw new InvalidArgumentException('Invalid input data type');
+ }
+ // find base event instance and remove it from events collection
+ foreach ($events as $key => $vevent) {
+ if (!isset($vevent->{'RECURRENCE-ID'})) {
+ $this->baseEvent = $vevent;
+ unset($events[$key]);
+ }
+ }
+
+ // No base event was found. CalDAV does allow cases where only
+ // overridden instances are stored.
+ //
+ // In this particular case, we're just going to grab the first
+ // event and use that instead. This may not always give the
+ // desired result.
+ if (!isset($this->baseEvent) && count($events) > 0) {
+ $this->baseEvent = array_shift($events);
+ }
+
+ // determine the event starting time zone
+ // we require this to align all other dates times
+ // evaluate if timezone parameter was used (treat this as a override)
+ if ($timeZone !== null) {
+ $this->baseEventStartTimeZone = $timeZone;
+ }
+ // evaluate if event start date has a timezone parameter
+ elseif (isset($this->baseEvent->DTSTART->parameters['TZID'])) {
+ $this->baseEventStartTimeZone = $timeZoneFactory->fromName($this->baseEvent->DTSTART->parameters['TZID']->getValue()) ?? new DateTimeZone('UTC');
+ }
+ // evaluate if event parent calendar has a time zone
+ elseif (isset($calendarTimeZone)) {
+ $this->baseEventStartTimeZone = clone $calendarTimeZone;
+ }
+ // otherwise, as a last resort use the UTC timezone
+ else {
+ $this->baseEventStartTimeZone = new DateTimeZone('UTC');
+ }
+
+ // determine the event end time zone
+ // we require this to align all other dates and times
+ // evaluate if timezone parameter was used (treat this as a override)
+ if ($timeZone !== null) {
+ $this->baseEventEndTimeZone = $timeZone;
+ }
+ // evaluate if event end date has a timezone parameter
+ elseif (isset($this->baseEvent->DTEND->parameters['TZID'])) {
+ $this->baseEventEndTimeZone = $timeZoneFactory->fromName($this->baseEvent->DTEND->parameters['TZID']->getValue()) ?? new DateTimeZone('UTC');
+ }
+ // evaluate if event parent calendar has a time zone
+ elseif (isset($calendarTimeZone)) {
+ $this->baseEventEndTimeZone = clone $calendarTimeZone;
+ }
+ // otherwise, as a last resort use the start date time zone
+ else {
+ $this->baseEventEndTimeZone = clone $this->baseEventStartTimeZone;
+ }
+ // extract start date and time
+ $this->baseEventStartDate = $this->baseEvent->DTSTART->getDateTime($this->baseEventStartTimeZone);
+ $this->baseEventStartDateFloating = $this->baseEvent->DTSTART->isFloating();
+ // determine event end date and duration
+ // evaluate if end date exists
+ // extract end date and calculate duration
+ if (isset($this->baseEvent->DTEND)) {
+ $this->baseEventEndDate = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone);
+ $this->baseEventEndDateFloating = $this->baseEvent->DTEND->isFloating();
+ $this->baseEventDuration
+ = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone)->getTimeStamp()
+ - $this->baseEventStartDate->getTimeStamp();
+ }
+ // evaluate if duration exists
+ // extract duration and calculate end date
+ elseif (isset($this->baseEvent->DURATION)) {
+ $this->baseEventEndDate = DateTimeImmutable::createFromInterface($this->baseEventStartDate)
+ ->add($this->baseEvent->DURATION->getDateInterval());
+ $this->baseEventDuration = $this->baseEventEndDate->getTimestamp() - $this->baseEventStartDate->getTimestamp();
+ }
+ // evaluate if start date is floating
+ // set duration to 24 hours and calculate the end date
+ // according to the rfc any event without a end date or duration is a complete day
+ elseif ($this->baseEventStartDateFloating == true) {
+ $this->baseEventDuration = 86400;
+ $this->baseEventEndDate = DateTimeImmutable::createFromInterface($this->baseEventStartDate)
+ ->setTimestamp($this->baseEventStartDate->getTimestamp() + $this->baseEventDuration);
+ }
+ // otherwise, set duration to zero this should never happen
+ else {
+ $this->baseEventDuration = 0;
+ $this->baseEventEndDate = $this->baseEventStartDate;
+ }
+ // evaluate if RRULE exist and construct iterator
+ if (isset($this->baseEvent->RRULE)) {
+ $this->rruleIterator = new EventReaderRRule(
+ $this->baseEvent->RRULE->getParts(),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if RDATE exist and construct iterator
+ if (isset($this->baseEvent->RDATE)) {
+ $dates = [];
+ foreach ($this->baseEvent->RDATE as $entry) {
+ $dates[] = $entry->getValue();
+ }
+ $this->rdateIterator = new EventReaderRDate(
+ implode(',', $dates),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if EXRULE exist and construct iterator
+ if (isset($this->baseEvent->EXRULE)) {
+ $this->eruleIterator = new EventReaderRRule(
+ $this->baseEvent->EXRULE->getParts(),
+ $this->baseEventStartDate
+ );
+ }
+ // evaluate if EXDATE exist and construct iterator
+ if (isset($this->baseEvent->EXDATE)) {
+ $dates = [];
+ foreach ($this->baseEvent->EXDATE as $entry) {
+ $dates[] = $entry->getValue();
+ }
+ $this->edateIterator = new EventReaderRDate(
+ implode(',', $dates),
+ $this->baseEventStartDate
+ );
+ }
+ // construct collection of modified events with recurrence id as hash
+ foreach ($events as $vevent) {
+ $this->recurrenceModified[$vevent->{'RECURRENCE-ID'}->getDateTime($this->baseEventStartTimeZone)->getTimeStamp()] = $vevent;
+ }
+
+ $this->recurrenceCurrentDate = clone $this->baseEventStartDate;
+ }
+
+ /**
+ * retrieve date and time of event start
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function startDateTime(): DateTime {
+ return DateTime::createFromInterface($this->baseEventStartDate);
+ }
+
+ /**
+ * retrieve time zone of event start
+ *
+ * @since 30.0.0
+ *
+ * @return DateTimeZone
+ */
+ public function startTimeZone(): DateTimeZone {
+ return $this->baseEventStartTimeZone;
+ }
+
+ /**
+ * retrieve date and time of event end
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function endDateTime(): DateTime {
+ return DateTime::createFromInterface($this->baseEventEndDate);
+ }
+
+ /**
+ * retrieve time zone of event end
+ *
+ * @since 30.0.0
+ *
+ * @return DateTimeZone
+ */
+ public function endTimeZone(): DateTimeZone {
+ return $this->baseEventEndTimeZone;
+ }
+
+ /**
+ * is this an all day event
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function entireDay(): bool {
+ return $this->baseEventStartDateFloating;
+ }
+
+ /**
+ * is this a recurring event
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function recurs(): bool {
+ return ($this->rruleIterator !== null || $this->rdateIterator !== null);
+ }
+
+ /**
+ * event recurrence pattern
+ *
+ * @since 30.0.0
+ *
+ * @return string|null R - Relative or A - Absolute
+ */
+ public function recurringPattern(): ?string {
+ if ($this->rruleIterator === null && $this->rdateIterator === null) {
+ return null;
+ }
+ if ($this->rruleIterator?->isRelative()) {
+ return 'R';
+ }
+ return 'A';
+ }
+
+ /**
+ * event recurrence precision
+ *
+ * @since 30.0.0
+ *
+ * @return string|null daily, weekly, monthly, yearly, fixed
+ */
+ public function recurringPrecision(): ?string {
+ if ($this->rruleIterator !== null) {
+ return $this->rruleIterator->precision();
+ }
+ if ($this->rdateIterator !== null) {
+ return 'fixed';
+ }
+ return null;
+ }
+
+ /**
+ * event recurrence interval
+ *
+ * @since 30.0.0
+ *
+ * @return int|null
+ */
+ public function recurringInterval(): ?int {
+ return $this->rruleIterator?->interval();
+ }
+
+ /**
+ * event recurrence conclusion
+ *
+ * returns true if RRULE with UNTIL or COUNT (calculated) is used
+ * returns true RDATE is used
+ * returns false if RRULE or RDATE are absent, or RRRULE is infinite
+ *
+ * @since 30.0.0
+ *
+ * @return bool
+ */
+ public function recurringConcludes(): bool {
+
+ // retrieve rrule conclusions
+ if ($this->rruleIterator?->concludesOn() !== null
+ || $this->rruleIterator?->concludesAfter() !== null) {
+ return true;
+ }
+ // retrieve rdate conclusions
+ if ($this->rdateIterator?->concludesAfter() !== null) {
+ return true;
+ }
+
+ return false;
+
+ }
+
+ /**
+ * event recurrence conclusion iterations
+ *
+ * returns the COUNT value if RRULE is used
+ * returns the collection count if RDATE is used
+ * returns combined count of RRULE COUNT and RDATE if both are used
+ * returns null if RRULE and RDATE are absent
+ *
+ * @since 30.0.0
+ *
+ * @return int|null
+ */
+ public function recurringConcludesAfter(): ?int {
+
+ // construct count place holder
+ $count = 0;
+ // retrieve and add RRULE iterations count
+ $count += (int)$this->rruleIterator?->concludesAfter();
+ // retrieve and add RDATE iterations count
+ $count += (int)$this->rdateIterator?->concludesAfter();
+ // return count
+ return !empty($count) ? $count : null;
+
+ }
+
+ /**
+ * event recurrence conclusion date
+ *
+ * returns the last date of UNTIL or COUNT (calculated) if RRULE is used
+ * returns the last date in the collection if RDATE is used
+ * returns the highest date if both RRULE and RDATE are used
+ * returns null if RRULE and RDATE are absent or RRULE is infinite
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime|null
+ */
+ public function recurringConcludesOn(): ?DateTime {
+
+ if ($this->rruleIterator !== null) {
+ // retrieve rrule conclusion date
+ $rrule = $this->rruleIterator->concludes();
+ // evaluate if rrule conclusion is null
+ // if this is null that means the recurrence is infinate
+ if ($rrule === null) {
+ return null;
+ }
+ }
+ // retrieve rdate conclusion date
+ if ($this->rdateIterator !== null) {
+ $rdate = $this->rdateIterator->concludes();
+ }
+ // evaluate if both rrule and rdate have date
+ if (isset($rdate) && isset($rrule)) {
+ // return the highest date
+ return (($rdate > $rrule) ? $rdate : $rrule);
+ } elseif (isset($rrule)) {
+ return $rrule;
+ } elseif (isset($rdate)) {
+ return $rdate;
+ }
+
+ return null;
+
+ }
+
+ /**
+ * event recurrence days of the week
+ *
+ * returns collection of RRULE BYDAY day(s) ['MO','WE','FR']
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfWeek(): array {
+ // evaluate if RRULE exists and return day(s) of the week
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
+ }
+
+ /**
+ * event recurrence days of the week (named)
+ *
+ * returns collection of RRULE BYDAY day(s) ['Monday','Wednesday','Friday']
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfWeekNamed(): array {
+ // evaluate if RRULE exists and extract day(s) of the week
+ $days = $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
+ // convert numberic month to month name
+ foreach ($days as $key => $value) {
+ $days[$key] = $this->dayNamesMap[$value];
+ }
+ // return names collection
+ return $days;
+ }
+
+ /**
+ * event recurrence days of the month
+ *
+ * returns collection of RRULE BYMONTHDAY day(s) [7, 15, 31]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfMonth(): array {
+ // evaluate if RRULE exists and return day(s) of the month
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfMonth() : [];
+ }
+
+ /**
+ * event recurrence days of the year
+ *
+ * returns collection of RRULE BYYEARDAY day(s) [57, 205, 365]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringDaysOfYear(): array {
+ // evaluate if RRULE exists and return day(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->daysOfYear() : [];
+ }
+
+ /**
+ * event recurrence weeks of the month
+ *
+ * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfMonth(): array {
+ // evaluate if RRULE exists and RRULE is relative return relative position(s)
+ return $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ }
+
+ /**
+ * event recurrence weeks of the month (named)
+ *
+ * returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfMonthNamed(): array {
+ // evaluate if RRULE exists and extract relative position(s)
+ $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ // convert numberic relative position to relative label
+ foreach ($positions as $key => $value) {
+ $positions[$key] = $this->relativePositionNamesMap[$value];
+ }
+ // return positions collection
+ return $positions;
+ }
+
+ /**
+ * event recurrence weeks of the year
+ *
+ * returns collection of RRULE BYWEEKNO weeks(s) [12, 32, 52]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringWeeksOfYear(): array {
+ // evaluate if RRULE exists and return weeks(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->weeksOfYear() : [];
+ }
+
+ /**
+ * event recurrence months of the year
+ *
+ * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringMonthsOfYear(): array {
+ // evaluate if RRULE exists and return month(s) of the year
+ return $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
+ }
+
+ /**
+ * event recurrence months of the year (named)
+ *
+ * returns collection of RRULE BYMONTH month(s) [3, 7, 12]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringMonthsOfYearNamed(): array {
+ // evaluate if RRULE exists and extract month(s) of the year
+ $months = $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
+ // convert numberic month to month name
+ foreach ($months as $key => $value) {
+ $months[$key] = $this->monthNamesMap[$value];
+ }
+ // return months collection
+ return $months;
+ }
+
+ /**
+ * event recurrence relative positions
+ *
+ * returns collection of RRULE SETPOS value(s) [1, 5, -3]
+ * returns blank collection if RRULE is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringRelativePosition(): array {
+ // evaluate if RRULE exists and return relative position(s)
+ return $this->rruleIterator !== null ? $this->rruleIterator->relativePosition() : [];
+ }
+
+ /**
+ * event recurrence relative positions (named)
+ *
+ * returns collection of RRULE SETPOS [1, 3, -1]
+ * returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
+ *
+ * @since 30.0.0
+ *
+ * @return array
+ */
+ public function recurringRelativePositionNamed(): array {
+ // evaluate if RRULE exists and extract relative position(s)
+ $positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
+ // convert numberic relative position to relative label
+ foreach ($positions as $key => $value) {
+ $positions[$key] = $this->relativePositionNamesMap[$value];
+ }
+ // return positions collection
+ return $positions;
+ }
+
+ /**
+ * event recurrence date
+ *
+ * returns date of currently selected recurrence
+ *
+ * @since 30.0.0
+ *
+ * @return DateTime
+ */
+ public function recurrenceDate(): ?DateTime {
+ if ($this->recurrenceCurrentDate !== null) {
+ return DateTime::createFromInterface($this->recurrenceCurrentDate);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * event recurrence rewind
+ *
+ * sets the current recurrence to the first recurrence in the collection
+ *
+ * @since 30.0.0
+ *
+ * @return void
+ */
+ public function recurrenceRewind(): void {
+ // rewind and increment rrule
+ if ($this->rruleIterator !== null) {
+ $this->rruleIterator->rewind();
+ }
+ // rewind and increment rdate
+ if ($this->rdateIterator !== null) {
+ $this->rdateIterator->rewind();
+ }
+ // rewind and increment exrule
+ if ($this->eruleIterator !== null) {
+ $this->eruleIterator->rewind();
+ }
+ // rewind and increment exdate
+ if ($this->edateIterator !== null) {
+ $this->edateIterator->rewind();
+ }
+ // set current date to event start date
+ $this->recurrenceCurrentDate = clone $this->baseEventStartDate;
+ }
+
+ /**
+ * event recurrence advance
+ *
+ * sets the current recurrence to the next recurrence in the collection
+ *
+ * @since 30.0.0
+ *
+ * @return void
+ */
+ public function recurrenceAdvance(): void {
+ // place holders
+ $nextOccurrenceDate = null;
+ $nextExceptionDate = null;
+ $rruleDate = null;
+ $rdateDate = null;
+ $eruleDate = null;
+ $edateDate = null;
+ // evaludate if rrule is set and advance one interation past current date
+ if ($this->rruleIterator !== null) {
+ // forward rrule to the next future date
+ while ($this->rruleIterator->valid() && $this->rruleIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->rruleIterator->next();
+ }
+ $rruleDate = $this->rruleIterator->current();
+ }
+ // evaludate if rdate is set and advance one interation past current date
+ if ($this->rdateIterator !== null) {
+ // forward rdate to the next future date
+ while ($this->rdateIterator->valid() && $this->rdateIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->rdateIterator->next();
+ }
+ $rdateDate = $this->rdateIterator->current();
+ }
+ if ($rruleDate !== null && $rdateDate !== null) {
+ $nextOccurrenceDate = ($rruleDate <= $rdateDate) ? $rruleDate : $rdateDate;
+ } elseif ($rruleDate !== null) {
+ $nextOccurrenceDate = $rruleDate;
+ } elseif ($rdateDate !== null) {
+ $nextOccurrenceDate = $rdateDate;
+ }
+
+ // evaludate if exrule is set and advance one interation past current date
+ if ($this->eruleIterator !== null) {
+ // forward exrule to the next future date
+ while ($this->eruleIterator->valid() && $this->eruleIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->eruleIterator->next();
+ }
+ $eruleDate = $this->eruleIterator->current();
+ }
+ // evaludate if exdate is set and advance one interation past current date
+ if ($this->edateIterator !== null) {
+ // forward exdate to the next future date
+ while ($this->edateIterator->valid() && $this->edateIterator->current() <= $this->recurrenceCurrentDate) {
+ $this->edateIterator->next();
+ }
+ $edateDate = $this->edateIterator->current();
+ }
+ // evaludate if exrule and exdate are set and set nextExDate to the first next date
+ if ($eruleDate !== null && $edateDate !== null) {
+ $nextExceptionDate = ($eruleDate <= $edateDate) ? $eruleDate : $edateDate;
+ } elseif ($eruleDate !== null) {
+ $nextExceptionDate = $eruleDate;
+ } elseif ($edateDate !== null) {
+ $nextExceptionDate = $edateDate;
+ }
+ // if the next date is part of exrule or exdate find another date
+ if ($nextOccurrenceDate !== null && $nextExceptionDate !== null && $nextOccurrenceDate == $nextExceptionDate) {
+ $this->recurrenceCurrentDate = $nextOccurrenceDate;
+ $this->recurrenceAdvance();
+ } else {
+ $this->recurrenceCurrentDate = $nextOccurrenceDate;
+ }
+ }
+
+ /**
+ * event recurrence advance
+ *
+ * sets the current recurrence to the next recurrence in the collection after the specific date
+ *
+ * @since 30.0.0
+ *
+ * @param DateTimeInterface $dt date and time to advance
+ *
+ * @return void
+ */
+ public function recurrenceAdvanceTo(DateTimeInterface $dt): void {
+ while ($this->recurrenceCurrentDate !== null && $this->recurrenceCurrentDate < $dt) {
+ $this->recurrenceAdvance();
+ }
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/EventReaderRDate.php b/apps/dav/lib/CalDAV/EventReaderRDate.php
new file mode 100644
index 00000000000..20234d06c00
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReaderRDate.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+
+class EventReaderRDate extends \Sabre\VObject\Recur\RDateIterator {
+
+ public function concludes(): ?DateTime {
+ return $this->concludesOn();
+ }
+
+ public function concludesAfter(): ?int {
+ return !empty($this->dates) ? count($this->dates) : null;
+ }
+
+ public function concludesOn(): ?DateTime {
+ if (count($this->dates) > 0) {
+ return new DateTime(
+ $this->dates[array_key_last($this->dates)],
+ $this->startDate->getTimezone()
+ );
+ }
+
+ return null;
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/EventReaderRRule.php b/apps/dav/lib/CalDAV/EventReaderRRule.php
new file mode 100644
index 00000000000..d2b4968c479
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventReaderRRule.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTime;
+use DateTimeInterface;
+
+class EventReaderRRule extends \Sabre\VObject\Recur\RRuleIterator {
+
+ public function precision(): string {
+ return $this->frequency;
+ }
+
+ public function interval(): int {
+ return $this->interval;
+ }
+
+ public function concludes(): ?DateTime {
+ // evaluate if until value is a date
+ if ($this->until instanceof DateTimeInterface) {
+ return DateTime::createFromInterface($this->until);
+ }
+ // evaluate if count value is higher than 0
+ if ($this->count > 0) {
+ // temporarily store current recurrence date and counter
+ $currentReccuranceDate = $this->currentDate;
+ $currentCounter = $this->counter;
+ // iterate over occurrences until last one (subtract 2 from count for start and end occurrence)
+ while ($this->counter <= ($this->count - 2)) {
+ $this->next();
+ }
+ // temporarly store last reccurance date
+ $lastReccuranceDate = $this->currentDate;
+ // restore current recurrence date and counter
+ $this->currentDate = $currentReccuranceDate;
+ $this->counter = $currentCounter;
+ // return last recurrence date
+ return DateTime::createFromInterface($lastReccuranceDate);
+ }
+
+ return null;
+ }
+
+ public function concludesAfter(): ?int {
+ return !empty($this->count) ? $this->count : null;
+ }
+
+ public function concludesOn(): ?DateTime {
+ return isset($this->until) ? DateTime::createFromInterface($this->until) : null;
+ }
+
+ public function daysOfWeek(): array {
+ return is_array($this->byDay) ? $this->byDay : [];
+ }
+
+ public function daysOfMonth(): array {
+ return is_array($this->byMonthDay) ? $this->byMonthDay : [];
+ }
+
+ public function daysOfYear(): array {
+ return is_array($this->byYearDay) ? $this->byYearDay : [];
+ }
+
+ public function weeksOfYear(): array {
+ return is_array($this->byWeekNo) ? $this->byWeekNo : [];
+ }
+
+ public function monthsOfYear(): array {
+ return is_array($this->byMonth) ? $this->byMonth : [];
+ }
+
+ public function isRelative(): bool {
+ return isset($this->bySetPos);
+ }
+
+ public function relativePosition(): array {
+ return is_array($this->bySetPos) ? $this->bySetPos : [];
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Export/ExportService.php b/apps/dav/lib/CalDAV/Export/ExportService.php
new file mode 100644
index 00000000000..552b9e2b675
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Export/ExportService.php
@@ -0,0 +1,107 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV\Export;
+
+use Generator;
+use OCP\Calendar\CalendarExportOptions;
+use OCP\Calendar\ICalendarExport;
+use OCP\ServerVersion;
+use Sabre\VObject\Component;
+use Sabre\VObject\Writer;
+
+/**
+ * Calendar Export Service
+ */
+class ExportService {
+
+ public const FORMATS = ['ical', 'jcal', 'xcal'];
+ private string $systemVersion;
+
+ public function __construct(ServerVersion $serverVersion) {
+ $this->systemVersion = $serverVersion->getVersionString();
+ }
+
+ /**
+ * Generates serialized content stream for a calendar and objects based in selected format
+ *
+ * @return Generator<string>
+ */
+ public function export(ICalendarExport $calendar, CalendarExportOptions $options): Generator {
+ // output start of serialized content based on selected format
+ yield $this->exportStart($options->getFormat());
+ // iterate through each returned vCalendar entry
+ // extract each component except timezones, convert to appropriate format and output
+ // extract any timezones and save them but do not output
+ $timezones = [];
+ foreach ($calendar->export($options) as $entry) {
+ $consecutive = false;
+ foreach ($entry->getComponents() as $vComponent) {
+ if ($vComponent->name === 'VTIMEZONE') {
+ if (isset($vComponent->TZID) && !isset($timezones[$vComponent->TZID->getValue()])) {
+ $timezones[$vComponent->TZID->getValue()] = clone $vComponent;
+ }
+ } else {
+ yield $this->exportObject($vComponent, $options->getFormat(), $consecutive);
+ $consecutive = true;
+ }
+ }
+ }
+ // iterate through each saved vTimezone entry, convert to appropriate format and output
+ foreach ($timezones as $vComponent) {
+ yield $this->exportObject($vComponent, $options->getFormat(), $consecutive);
+ $consecutive = true;
+ }
+ // output end of serialized content based on selected format
+ yield $this->exportFinish($options->getFormat());
+ }
+
+ /**
+ * Generates serialized content start based on selected format
+ */
+ private function exportStart(string $format): string {
+ return match ($format) {
+ 'jcal' => '["vcalendar",[["version",{},"text","2.0"],["prodid",{},"text","-\/\/IDN nextcloud.com\/\/Calendar Export v' . $this->systemVersion . '\/\/EN"]],[',
+ 'xcal' => '<?xml version="1.0" encoding="UTF-8"?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><version><text>2.0</text></version><prodid><text>-//IDN nextcloud.com//Calendar Export v' . $this->systemVersion . '//EN</text></prodid></properties><components>',
+ default => "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//IDN nextcloud.com//Calendar Export v" . $this->systemVersion . "//EN\n"
+ };
+ }
+
+ /**
+ * Generates serialized content end based on selected format
+ */
+ private function exportFinish(string $format): string {
+ return match ($format) {
+ 'jcal' => ']]',
+ 'xcal' => '</components></vcalendar></icalendar>',
+ default => "END:VCALENDAR\n"
+ };
+ }
+
+ /**
+ * Generates serialized content for a component based on selected format
+ */
+ private function exportObject(Component $vobject, string $format, bool $consecutive): string {
+ return match ($format) {
+ 'jcal' => $consecutive ? ',' . Writer::writeJson($vobject) : Writer::writeJson($vobject),
+ 'xcal' => $this->exportObjectXml($vobject),
+ default => Writer::write($vobject)
+ };
+ }
+
+ /**
+ * Generates serialized content for a component in xml format
+ */
+ private function exportObjectXml(Component $vobject): string {
+ $writer = new \Sabre\Xml\Writer();
+ $writer->openMemory();
+ $writer->setIndent(false);
+ $vobject->xmlSerialize($writer);
+ return $writer->outputMemory();
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
index 2b4f8ed0223..08dc10f7bf4 100644
--- a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
+++ b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -18,18 +19,16 @@ use Sabre\VObject\Property\ICalendar\Duration;
* @package OCA\DAV\CalDAV\ICSExportPlugin
*/
class ICSExportPlugin extends \Sabre\CalDAV\ICSExportPlugin {
- private IConfig $config;
- private LoggerInterface $logger;
-
/** @var string */
private const DEFAULT_REFRESH_INTERVAL = 'PT4H';
/**
* ICSExportPlugin constructor.
*/
- public function __construct(IConfig $config, LoggerInterface $logger) {
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
index 882a11c34b2..acf81638679 100644
--- a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
+++ b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -32,21 +33,16 @@ abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties {
*/
private const DELIMITER = '--';
- /** @var string */
- private $appId;
-
- /** @var string */
- private $calendarUri;
-
/**
* ExternalCalendar constructor.
*
* @param string $appId
* @param string $calendarUri
*/
- public function __construct(string $appId, string $calendarUri) {
- $this->appId = $appId;
- $this->calendarUri = $calendarUri;
+ public function __construct(
+ private string $appId,
+ private string $calendarUri,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
index bbee4cbda7c..40a8860dcb4 100644
--- a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
+++ b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
index 94f95652677..c8a7109abde 100644
--- a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
+++ b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -8,13 +9,23 @@ namespace OCA\DAV\CalDAV\InvitationResponse;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\Auth\PublicPrincipalPlugin;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\Publishing\PublishPlugin;
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\DavAclPlugin;
+use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
+use OCA\DAV\Connector\Sabre\LockPlugin;
+use OCA\DAV\Connector\Sabre\MaintenancePlugin;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\DAV\RootCollection;
+use OCA\Theming\ThemingDefaults;
+use OCP\App\IAppManager;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
+use OCP\IURLGenerator;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Sabre\VObject\ITip\Message;
@@ -27,21 +38,23 @@ class InvitationResponseServer {
*/
public function __construct(bool $public = true) {
$baseUri = \OC::$WEBROOT . '/remote.php/dav/';
- $logger = \OC::$server->get(LoggerInterface::class);
- /** @var IEventDispatcher $dispatcher */
- $dispatcher = \OC::$server->query(IEventDispatcher::class);
+ $logger = Server::get(LoggerInterface::class);
+ $dispatcher = Server::get(IEventDispatcher::class);
$root = new RootCollection();
$this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
// Add maintenance plugin
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav')));
+ $this->server->addPlugin(new MaintenancePlugin(Server::get(IConfig::class), \OC::$server->getL10N('dav')));
// Set URL explicitly due to reverse-proxy situations
$this->server->httpRequest->setUrl($baseUri);
$this->server->setBaseUri($baseUri);
- $this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
+ $this->server->addPlugin(new BlockLegacyClientPlugin(
+ Server::get(IConfig::class),
+ Server::get(ThemingDefaults::class),
+ ));
$this->server->addPlugin(new AnonymousOptionsPlugin());
// allow custom principal uri option
@@ -55,8 +68,8 @@ class InvitationResponseServer {
$event = new SabrePluginAuthInitEvent($this->server);
$dispatcher->dispatchTyped($event);
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger));
- $this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
+ $this->server->addPlugin(new ExceptionLoggerPlugin('webdav', $logger));
+ $this->server->addPlugin(new LockPlugin());
$this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
// acl
@@ -69,13 +82,13 @@ class InvitationResponseServer {
// calendar plugins
$this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
- $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class)));
+ $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(Server::get(IConfig::class), Server::get(LoggerInterface::class), Server::get(DefaultCalendarValidator::class)));
$this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
//$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
- $this->server->addPlugin(new \OCA\DAV\CalDAV\Publishing\PublishPlugin(
- \OC::$server->getConfig(),
- \OC::$server->getURLGenerator()
+ $this->server->addPlugin(new PublishPlugin(
+ Server::get(IConfig::class),
+ Server::get(IURLGenerator::class)
));
// wait with registering these until auth is handled and the filesystem is setup
@@ -83,7 +96,7 @@ class InvitationResponseServer {
// register plugins from apps
$pluginManager = new PluginManager(
\OC::$server,
- \OC::$server->getAppManager()
+ Server::get(IAppManager::class)
);
foreach ($pluginManager->getAppPlugins() as $appPlugin) {
$this->server->addPlugin($appPlugin);
diff --git a/apps/dav/lib/CalDAV/Outbox.php b/apps/dav/lib/CalDAV/Outbox.php
index ffda73147a6..608114d8093 100644
--- a/apps/dav/lib/CalDAV/Outbox.php
+++ b/apps/dav/lib/CalDAV/Outbox.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -15,9 +16,6 @@ use Sabre\CalDAV\Plugin as CalDAVPlugin;
*/
class Outbox extends \Sabre\CalDAV\Schedule\Outbox {
- /** @var IConfig */
- private $config;
-
/** @var null|bool */
private $disableFreeBusy = null;
@@ -27,9 +25,11 @@ class Outbox extends \Sabre\CalDAV\Schedule\Outbox {
* @param IConfig $config
* @param string $principalUri
*/
- public function __construct(IConfig $config, string $principalUri) {
+ public function __construct(
+ private IConfig $config,
+ string $principalUri,
+ ) {
parent::__construct($principalUri);
- $this->config = $config;
}
/**
diff --git a/apps/dav/lib/CalDAV/Principal/Collection.php b/apps/dav/lib/CalDAV/Principal/Collection.php
index f2cea0b5136..b76fde66464 100644
--- a/apps/dav/lib/CalDAV/Principal/Collection.php
+++ b/apps/dav/lib/CalDAV/Principal/Collection.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Principal/User.php b/apps/dav/lib/CalDAV/Principal/User.php
index 60b7953ea62..047d83827ed 100644
--- a/apps/dav/lib/CalDAV/Principal/User.php
+++ b/apps/dav/lib/CalDAV/Principal/User.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Proxy/Proxy.php b/apps/dav/lib/CalDAV/Proxy/Proxy.php
index a10f82e4b29..ef1ad8c634f 100644
--- a/apps/dav/lib/CalDAV/Proxy/Proxy.php
+++ b/apps/dav/lib/CalDAV/Proxy/Proxy.php
@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Proxy;
use OCP\AppFramework\Db\Entity;
+use OCP\DB\Types;
/**
* @method string getOwnerId()
@@ -28,8 +29,8 @@ class Proxy extends Entity {
protected $permissions;
public function __construct() {
- $this->addType('ownerId', 'string');
- $this->addType('proxyId', 'string');
- $this->addType('permissions', 'int');
+ $this->addType('ownerId', Types::STRING);
+ $this->addType('proxyId', Types::STRING);
+ $this->addType('permissions', Types::INTEGER);
}
}
diff --git a/apps/dav/lib/CalDAV/PublicCalendar.php b/apps/dav/lib/CalDAV/PublicCalendar.php
index 4ee811efeae..9af6e544165 100644
--- a/apps/dav/lib/CalDAV/PublicCalendar.php
+++ b/apps/dav/lib/CalDAV/PublicCalendar.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/PublicCalendarObject.php b/apps/dav/lib/CalDAV/PublicCalendarObject.php
index c3dc5ab1843..2ab40b94347 100644
--- a/apps/dav/lib/CalDAV/PublicCalendarObject.php
+++ b/apps/dav/lib/CalDAV/PublicCalendarObject.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/PublicCalendarRoot.php b/apps/dav/lib/CalDAV/PublicCalendarRoot.php
index e7008bfd82c..edfb9f8dccc 100644
--- a/apps/dav/lib/CalDAV/PublicCalendarRoot.php
+++ b/apps/dav/lib/CalDAV/PublicCalendarRoot.php
@@ -14,18 +14,6 @@ use Sabre\DAV\Collection;
class PublicCalendarRoot extends Collection {
- /** @var CalDavBackend */
- protected $caldavBackend;
-
- /** @var \OCP\IL10N */
- protected $l10n;
-
- /** @var \OCP\IConfig */
- protected $config;
-
- /** @var LoggerInterface */
- private $logger;
-
/**
* PublicCalendarRoot constructor.
*
@@ -33,12 +21,12 @@ class PublicCalendarRoot extends Collection {
* @param IL10N $l10n
* @param IConfig $config
*/
- public function __construct(CalDavBackend $caldavBackend, IL10N $l10n,
- IConfig $config, LoggerInterface $logger) {
- $this->caldavBackend = $caldavBackend;
- $this->l10n = $l10n;
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ protected CalDavBackend $caldavBackend,
+ protected IL10N $l10n,
+ protected IConfig $config,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php
index 94c5b7d1f52..76378e7a1c5 100644
--- a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php
+++ b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -7,6 +8,7 @@ namespace OCA\DAV\CalDAV\Publishing;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\Publishing\Xml\Publisher;
+use OCP\AppFramework\Http;
use OCP\IConfig;
use OCP\IURLGenerator;
use Sabre\CalDAV\Xml\Property\AllowedSharingModes;
@@ -29,28 +31,21 @@ class PublishPlugin extends ServerPlugin {
protected $server;
/**
- * Config instance to get instance secret.
- *
- * @var IConfig
- */
- protected $config;
-
- /**
- * URL Generator for absolute URLs.
- *
- * @var IURLGenerator
- */
- protected $urlGenerator;
-
- /**
* PublishPlugin constructor.
*
* @param IConfig $config
* @param IURLGenerator $urlGenerator
*/
- public function __construct(IConfig $config, IURLGenerator $urlGenerator) {
- $this->config = $config;
- $this->urlGenerator = $urlGenerator;
+ public function __construct(
+ /**
+ * Config instance to get instance secret.
+ */
+ protected IConfig $config,
+ /**
+ * URL Generator for absolute URLs.
+ */
+ protected IURLGenerator $urlGenerator,
+ ) {
}
/**
@@ -97,17 +92,17 @@ class PublishPlugin extends ServerPlugin {
public function propFind(PropFind $propFind, INode $node) {
if ($node instanceof Calendar) {
- $propFind->handle('{'.self::NS_CALENDARSERVER.'}publish-url', function () use ($node) {
+ $propFind->handle('{' . self::NS_CALENDARSERVER . '}publish-url', function () use ($node) {
if ($node->getPublishStatus()) {
// We return the publish-url only if the calendar is published.
$token = $node->getPublishStatus();
- $publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri().'public-calendars/').$token;
+ $publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri() . 'public-calendars/') . $token;
return new Publisher($publishUrl, true);
}
});
- $propFind->handle('{'.self::NS_CALENDARSERVER.'}allowed-sharing-modes', function () use ($node) {
+ $propFind->handle('{' . self::NS_CALENDARSERVER . '}allowed-sharing-modes', function () use ($node) {
$canShare = (!$node->isSubscription() && $node->canWrite());
$canPublish = (!$node->isSubscription() && $node->canWrite());
@@ -133,7 +128,7 @@ class PublishPlugin extends ServerPlugin {
$path = $request->getPath();
// Only handling xml
- $contentType = (string) $request->getHeader('Content-Type');
+ $contentType = (string)$request->getHeader('Content-Type');
if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -160,7 +155,7 @@ class PublishPlugin extends ServerPlugin {
switch ($documentType) {
- case '{'.self::NS_CALENDARSERVER.'}publish-calendar':
+ case '{' . self::NS_CALENDARSERVER . '}publish-calendar':
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
@@ -186,7 +181,7 @@ class PublishPlugin extends ServerPlugin {
$node->setPublishStatus(true);
// iCloud sends back the 202, so we will too.
- $response->setStatus(202);
+ $response->setStatus(Http::STATUS_ACCEPTED);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
@@ -195,7 +190,7 @@ class PublishPlugin extends ServerPlugin {
// Breaking the event chain
return false;
- case '{'.self::NS_CALENDARSERVER.'}unpublish-calendar':
+ case '{' . self::NS_CALENDARSERVER . '}unpublish-calendar':
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
@@ -220,7 +215,7 @@ class PublishPlugin extends ServerPlugin {
$node->setPublishStatus(false);
- $response->setStatus(200);
+ $response->setStatus(Http::STATUS_OK);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
diff --git a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
index d3543ce5bae..fb9b7298f9b 100644
--- a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
+++ b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -11,22 +12,13 @@ use Sabre\Xml\XmlSerializable;
class Publisher implements XmlSerializable {
/**
- * @var string $publishUrl
- */
- protected $publishUrl;
-
- /**
- * @var boolean $isPublished
- */
- protected $isPublished;
-
- /**
* @param string $publishUrl
* @param boolean $isPublished
*/
- public function __construct($publishUrl, $isPublished) {
- $this->publishUrl = $publishUrl;
- $this->isPublished = $isPublished;
+ public function __construct(
+ protected $publishUrl,
+ protected $isPublished,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/Reminder/Backend.php b/apps/dav/lib/CalDAV/Reminder/Backend.php
index ce4469228d3..329af3a2f56 100644
--- a/apps/dav/lib/CalDAV/Reminder/Backend.php
+++ b/apps/dav/lib/CalDAV/Reminder/Backend.php
@@ -18,22 +18,16 @@ use OCP\IDBConnection;
*/
class Backend {
- /** @var IDBConnection */
- protected $db;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
/**
* Backend constructor.
*
* @param IDBConnection $db
* @param ITimeFactory $timeFactory
*/
- public function __construct(IDBConnection $db,
- ITimeFactory $timeFactory) {
- $this->db = $db;
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ protected IDBConnection $db,
+ protected ITimeFactory $timeFactory,
+ ) {
}
/**
@@ -44,12 +38,13 @@ class Backend {
*/
public function getRemindersToProcess():array {
$query = $this->db->getQueryBuilder();
- $query->select(['cr.*', 'co.calendardata', 'c.displayname', 'c.principaluri'])
+ $query->select(['cr.id', 'cr.calendar_id','cr.object_id','cr.is_recurring','cr.uid','cr.recurrence_id','cr.is_recurrence_exception','cr.event_hash','cr.alarm_hash','cr.type','cr.is_relative','cr.notification_date','cr.is_repeat_based','co.calendardata', 'c.displayname', 'c.principaluri'])
->from('calendar_reminders', 'cr')
->where($query->expr()->lte('cr.notification_date', $query->createNamedParameter($this->timeFactory->getTime())))
->join('cr', 'calendarobjects', 'co', $query->expr()->eq('cr.object_id', 'co.id'))
- ->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id'));
- $stmt = $query->execute();
+ ->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id'))
+ ->groupBy('cr.event_hash', 'cr.notification_date', 'cr.type', 'cr.id', 'cr.calendar_id', 'cr.object_id', 'cr.is_recurring', 'cr.uid', 'cr.recurrence_id', 'cr.is_recurrence_exception', 'cr.alarm_hash', 'cr.is_relative', 'cr.is_repeat_based', 'co.calendardata', 'c.displayname', 'c.principaluri');
+ $stmt = $query->executeQuery();
return array_map(
[$this, 'fixRowTyping'],
@@ -68,7 +63,7 @@ class Backend {
$query->select('*')
->from('calendar_reminders')
->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
- $stmt = $query->execute();
+ $stmt = $query->executeQuery();
return array_map(
[$this, 'fixRowTyping'],
@@ -121,7 +116,7 @@ class Backend {
'notification_date' => $query->createNamedParameter($notificationDate),
'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0),
])
- ->execute();
+ ->executeStatement();
return $query->getLastInsertId();
}
@@ -138,7 +133,7 @@ class Backend {
$query->update('calendar_reminders')
->set('notification_date', $query->createNamedParameter($newNotificationDate))
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -152,7 +147,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -165,7 +160,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -179,7 +174,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('calendar_id', $query->createNamedParameter($calendarId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -187,15 +182,15 @@ class Backend {
* @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'];
+ $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 $row;
}
diff --git a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
index 1428eb9c46c..31d60f1531d 100644
--- a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
@@ -29,6 +29,6 @@ interface INotificationProvider {
*/
public function send(VEvent $vevent,
?string $calendarDisplayName,
- array $principalEmailAddresses,
+ array $principalEmailAddresses,
array $users = []): void;
}
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
index 4d087f58d2b..94edff98e52 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
@@ -29,31 +29,18 @@ abstract class AbstractProvider implements INotificationProvider {
/** @var string */
public const NOTIFICATION_TYPE = '';
- protected LoggerInterface $logger;
-
- /** @var L10NFactory */
- protected $l10nFactory;
-
/** @var IL10N[] */
private $l10ns;
/** @var string */
private $fallbackLanguage;
- /** @var IURLGenerator */
- protected $urlGenerator;
-
- /** @var IConfig */
- protected $config;
-
- public function __construct(LoggerInterface $logger,
- L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator,
- IConfig $config) {
- $this->logger = $logger;
- $this->l10nFactory = $l10nFactory;
- $this->urlGenerator = $urlGenerator;
- $this->config = $config;
+ public function __construct(
+ protected LoggerInterface $logger,
+ protected L10NFactory $l10nFactory,
+ protected IURLGenerator $urlGenerator,
+ protected IConfig $config,
+ ) {
}
/**
@@ -113,7 +100,7 @@ abstract class AbstractProvider implements INotificationProvider {
*/
private function getStatusOfEvent(VEvent $vevent):string {
if ($vevent->STATUS) {
- return (string) $vevent->STATUS;
+ return (string)$vevent->STATUS;
}
// Doesn't say so in the standard,
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
index 947d286643c..0fd39a9e459 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
@@ -17,6 +17,7 @@ use OCP\L10N\IFactory as L10NFactory;
use OCP\Mail\Headers\AutoSubmitted;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
+use OCP\Util;
use Psr\Log\LoggerInterface;
use Sabre\VObject;
use Sabre\VObject\Component\VEvent;
@@ -32,15 +33,14 @@ class EmailProvider extends AbstractProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'EMAIL';
- private IMailer $mailer;
-
- public function __construct(IConfig $config,
- IMailer $mailer,
+ public function __construct(
+ IConfig $config,
+ private IMailer $mailer,
LoggerInterface $logger,
L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator) {
+ IURLGenerator $urlGenerator,
+ ) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
- $this->mailer = $mailer;
}
/**
@@ -87,7 +87,7 @@ class EmailProvider extends AbstractProvider {
$lang = $fallbackLanguage;
}
$l10n = $this->getL10NForLang($lang);
- $fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
+ $fromEMail = Util::getDefaultEmailAddress('reminders-noreply');
$template = $this->mailer->createEMailTemplate('dav.calendarReminder');
$template->addHeader();
@@ -149,11 +149,11 @@ class EmailProvider extends AbstractProvider {
$this->getAbsoluteImagePath('places/calendar.png'));
if (isset($vevent->LOCATION)) {
- $template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
+ $template->addBodyListItem((string)$vevent->LOCATION, $l10n->t('Where:'),
$this->getAbsoluteImagePath('actions/address.png'));
}
if (isset($vevent->DESCRIPTION)) {
- $template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
+ $template->addBodyListItem((string)$vevent->DESCRIPTION, $l10n->t('Description:'),
$this->getAbsoluteImagePath('actions/more.png'));
}
}
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
index 5b0ba28a6c1..a3f0cce547a 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
@@ -30,21 +30,15 @@ class PushProvider extends AbstractProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'DISPLAY';
- /** @var IManager */
- private $manager;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- public function __construct(IConfig $config,
- IManager $manager,
+ public function __construct(
+ IConfig $config,
+ private IManager $manager,
LoggerInterface $logger,
L10NFactory $l10nFactory,
IURLGenerator $urlGenerator,
- ITimeFactory $timeFactory) {
+ private ITimeFactory $timeFactory,
+ ) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
- $this->manager = $manager;
- $this->timeFactory = $timeFactory;
}
/**
@@ -65,7 +59,7 @@ class PushProvider extends AbstractProvider {
}
$eventDetails = $this->extractEventDetails($vevent);
- $eventUUID = (string) $vevent->UID;
+ $eventUUID = (string)$vevent->UID;
if (!$eventUUID) {
return;
};
@@ -100,13 +94,13 @@ class PushProvider extends AbstractProvider {
return [
'title' => isset($vevent->SUMMARY)
- ? ((string) $vevent->SUMMARY)
+ ? ((string)$vevent->SUMMARY)
: null,
'description' => isset($vevent->DESCRIPTION)
- ? ((string) $vevent->DESCRIPTION)
+ ? ((string)$vevent->DESCRIPTION)
: null,
'location' => isset($vevent->LOCATION)
- ? ((string) $vevent->LOCATION)
+ ? ((string)$vevent->LOCATION)
: null,
'all_day' => $start instanceof Property\ICalendar\Date,
'start_atom' => $start->getDateTime()->format(\DateTimeInterface::ATOM),
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
index f64de85c449..265db09b061 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
@@ -8,6 +8,10 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CalDAV\Reminder;
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException;
+use OCP\AppFramework\QueryException;
+use OCP\Server;
+
/**
* Class NotificationProviderManager
*
@@ -42,7 +46,7 @@ class NotificationProviderManager {
if (isset($this->providers[$type])) {
return $this->providers[$type];
}
- throw new NotificationProvider\ProviderNotAvailableException($type);
+ throw new ProviderNotAvailableException($type);
}
throw new NotificationTypeDoesNotExistException($type);
}
@@ -51,10 +55,10 @@ class NotificationProviderManager {
* Registers a new provider
*
* @param string $providerClassName
- * @throws \OCP\AppFramework\QueryException
+ * @throws QueryException
*/
public function registerProvider(string $providerClassName):void {
- $provider = \OC::$server->query($providerClassName);
+ $provider = Server::get($providerClassName);
if (!$provider instanceof INotificationProvider) {
throw new \InvalidArgumentException('Invalid notification provider registered');
diff --git a/apps/dav/lib/CalDAV/Reminder/Notifier.php b/apps/dav/lib/CalDAV/Reminder/Notifier.php
index f3c784ea21f..137fb509f56 100644
--- a/apps/dav/lib/CalDAV/Reminder/Notifier.php
+++ b/apps/dav/lib/CalDAV/Reminder/Notifier.php
@@ -26,31 +26,21 @@ use OCP\Notification\UnknownNotificationException;
*/
class Notifier implements INotifier {
- /** @var IFactory */
- private $l10nFactory;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
/** @var IL10N */
private $l10n;
- /** @var ITimeFactory */
- private $timeFactory;
-
/**
* Notifier constructor.
*
- * @param IFactory $factory
+ * @param IFactory $l10nFactory
* @param IURLGenerator $urlGenerator
* @param ITimeFactory $timeFactory
*/
- public function __construct(IFactory $factory,
- IURLGenerator $urlGenerator,
- ITimeFactory $timeFactory) {
- $this->l10nFactory = $factory;
- $this->urlGenerator = $urlGenerator;
- $this->timeFactory = $timeFactory;
+ public function __construct(
+ private IFactory $l10nFactory,
+ private IURLGenerator $urlGenerator,
+ private ITimeFactory $timeFactory,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php
index 6faf5bdecd6..c75090e1560 100644
--- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php
+++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php
@@ -32,33 +32,6 @@ use function strcasecmp;
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;
-
- /** @var IConfig */
- private $config;
-
- /** @var LoggerInterface */
- private $logger;
-
- /** @var Principal */
- private $principalConnector;
-
public const REMINDER_TYPE_EMAIL = 'EMAIL';
public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
public const REMINDER_TYPE_AUDIO = 'AUDIO';
@@ -74,24 +47,17 @@ class ReminderService {
self::REMINDER_TYPE_AUDIO
];
- public function __construct(Backend $backend,
- NotificationProviderManager $notificationProviderManager,
- IUserManager $userManager,
- IGroupManager $groupManager,
- CalDavBackend $caldavBackend,
- ITimeFactory $timeFactory,
- IConfig $config,
- LoggerInterface $logger,
- Principal $principalConnector) {
- $this->backend = $backend;
- $this->notificationProviderManager = $notificationProviderManager;
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->caldavBackend = $caldavBackend;
- $this->timeFactory = $timeFactory;
- $this->config = $config;
- $this->logger = $logger;
- $this->principalConnector = $principalConnector;
+ public function __construct(
+ private Backend $backend,
+ private NotificationProviderManager $notificationProviderManager,
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private CalDavBackend $caldavBackend,
+ private ITimeFactory $timeFactory,
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private Principal $principalConnector,
+ ) {
}
/**
@@ -206,14 +172,14 @@ class ReminderService {
if (!$vcalendar) {
return;
}
- $calendarTimeZone = $this->getCalendarTimeZone((int) $objectData['calendarid']);
+ $calendarTimeZone = $this->getCalendarTimeZone((int)$objectData['calendarid']);
$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
if (count($vevents) === 0) {
return;
}
- $uid = (string) $vevents[0]->UID;
+ $uid = (string)$vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
$now = $this->timeFactory->getDateTime();
@@ -283,7 +249,7 @@ class ReminderService {
continue;
}
- if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
+ 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;
@@ -353,7 +319,7 @@ class ReminderService {
return;
}
- $this->backend->cleanRemindersForEvent((int) $objectData['id']);
+ $this->backend->cleanRemindersForEvent((int)$objectData['id']);
}
/**
@@ -402,19 +368,19 @@ class ReminderService {
$alarms[] = [
'calendar_id' => $objectData['calendarid'],
'object_id' => $objectData['id'],
- 'uid' => (string) $valarm->parent->UID,
+ '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,
+ 'type' => (string)$valarm->ACTION,
'is_relative' => $isRelative,
'notification_date' => $notificationDate->getTimestamp(),
'is_repeat_based' => false,
];
- $repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
+ $repeat = isset($valarm->REPEAT) ? (int)$valarm->REPEAT->getValue() : 0;
for ($i = 0; $i < $repeat; $i++) {
if ($valarm->DURATION === null) {
continue;
@@ -424,13 +390,13 @@ class ReminderService {
$alarms[] = [
'calendar_id' => $objectData['calendarid'],
'object_id' => $objectData['id'],
- 'uid' => (string) $valarm->parent->UID,
+ '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,
+ 'type' => (string)$valarm->ACTION,
'is_relative' => $isRelative,
'notification_date' => $clonedNotificationDate->getTimestamp(),
'is_repeat_based' => true,
@@ -444,19 +410,26 @@ class ReminderService {
* @param array $reminders
*/
private function writeRemindersToDatabase(array $reminders): void {
+ $uniqueReminders = [];
foreach ($reminders as $reminder) {
+ $key = $reminder['notification_date'] . $reminder['event_hash'] . $reminder['type'];
+ if (!isset($uniqueReminders[$key])) {
+ $uniqueReminders[$key] = $reminder;
+ }
+ }
+ foreach (array_values($uniqueReminders) as $reminder) {
$this->backend->insertReminder(
- (int) $reminder['calendar_id'],
- (int) $reminder['object_id'],
+ (int)$reminder['calendar_id'],
+ (int)$reminder['object_id'],
$reminder['uid'],
$reminder['is_recurring'],
- (int) $reminder['recurrence_id'],
+ (int)$reminder['recurrence_id'],
$reminder['is_recurrence_exception'],
$reminder['event_hash'],
$reminder['alarm_hash'],
$reminder['type'],
$reminder['is_relative'],
- (int) $reminder['notification_date'],
+ (int)$reminder['notification_date'],
$reminder['is_repeat_based']
);
}
@@ -468,10 +441,10 @@ class ReminderService {
*/
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']) {
+ if ($reminder['is_repeat_based']
+ || !$reminder['is_recurring']
+ || !$reminder['is_relative']
+ || $reminder['is_recurrence_exception']) {
$this->backend->removeReminder($reminder['id']);
return;
}
@@ -479,7 +452,7 @@ class ReminderService {
$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$now = $this->timeFactory->getDateTime();
- $calendarTimeZone = $this->getCalendarTimeZone((int) $reminder['calendar_id']);
+ $calendarTimeZone = $this->getCalendarTimeZone((int)$reminder['calendar_id']);
try {
$iterator = new EventIterator($vevents, $reminder['uid']);
@@ -595,26 +568,26 @@ class ReminderService {
*/
private function getEventHash(VEvent $vevent):string {
$properties = [
- (string) $vevent->DTSTART->serialize(),
+ (string)$vevent->DTSTART->serialize(),
];
if ($vevent->DTEND) {
- $properties[] = (string) $vevent->DTEND->serialize();
+ $properties[] = (string)$vevent->DTEND->serialize();
}
if ($vevent->DURATION) {
- $properties[] = (string) $vevent->DURATION->serialize();
+ $properties[] = (string)$vevent->DURATION->serialize();
}
if ($vevent->{'RECURRENCE-ID'}) {
- $properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
+ $properties[] = (string)$vevent->{'RECURRENCE-ID'}->serialize();
}
if ($vevent->RRULE) {
- $properties[] = (string) $vevent->RRULE->serialize();
+ $properties[] = (string)$vevent->RRULE->serialize();
}
if ($vevent->EXDATE) {
- $properties[] = (string) $vevent->EXDATE->serialize();
+ $properties[] = (string)$vevent->EXDATE->serialize();
}
if ($vevent->RDATE) {
- $properties[] = (string) $vevent->RDATE->serialize();
+ $properties[] = (string)$vevent->RDATE->serialize();
}
return md5(implode('::', $properties));
@@ -629,15 +602,15 @@ class ReminderService {
*/
private function getAlarmHash(VAlarm $valarm):string {
$properties = [
- (string) $valarm->ACTION->serialize(),
- (string) $valarm->TRIGGER->serialize(),
+ (string)$valarm->ACTION->serialize(),
+ (string)$valarm->TRIGGER->serialize(),
];
if ($valarm->DURATION) {
- $properties[] = (string) $valarm->DURATION->serialize();
+ $properties[] = (string)$valarm->DURATION->serialize();
}
if ($valarm->REPEAT) {
- $properties[] = (string) $valarm->REPEAT->serialize();
+ $properties[] = (string)$valarm->REPEAT->serialize();
}
return md5(implode('::', $properties));
@@ -657,7 +630,7 @@ class ReminderService {
return null;
}
- $uid = (string) $vevents[0]->UID;
+ $uid = (string)$vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
@@ -708,7 +681,7 @@ class ReminderService {
*/
private function getStatusOfEvent(VEvent $vevent):string {
if ($vevent->STATUS) {
- return (string) $vevent->STATUS;
+ return (string)$vevent->STATUS;
}
// Doesn't say so in the standard,
@@ -847,7 +820,7 @@ class ReminderService {
private function getCalendarTimeZone(int $calendarid): DateTimeZone {
$calendarInfo = $this->caldavBackend->getCalendarById($calendarid);
$tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone';
- if (!isset($calendarInfo[$tzProp])) {
+ if (empty($calendarInfo[$tzProp])) {
// Defaulting to UTC
return new DateTimeZone('UTC');
}
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
index 2eb8ebfc84e..68bb3373346 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -25,23 +26,6 @@ use function array_values;
abstract class AbstractPrincipalBackend implements BackendInterface {
- /** @var IDBConnection */
- private $db;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IGroupManager */
- private $groupManager;
-
- private LoggerInterface $logger;
-
- /** @var ProxyMapper */
- private $proxyMapper;
-
- /** @var string */
- private $principalPrefix;
-
/** @var string */
private $dbTableName;
@@ -51,27 +35,19 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
/** @var string */
private $dbForeignKeyName;
- /** @var string */
- private $cuType;
-
- public function __construct(IDBConnection $dbConnection,
- IUserSession $userSession,
- IGroupManager $groupManager,
- LoggerInterface $logger,
- ProxyMapper $proxyMapper,
- string $principalPrefix,
+ public function __construct(
+ private IDBConnection $db,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ private LoggerInterface $logger,
+ private ProxyMapper $proxyMapper,
+ private string $principalPrefix,
string $dbPrefix,
- string $cuType) {
- $this->db = $dbConnection;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
- $this->logger = $logger;
- $this->proxyMapper = $proxyMapper;
- $this->principalPrefix = $principalPrefix;
+ private string $cuType,
+ ) {
$this->dbTableName = 'calendar_' . $dbPrefix . 's';
$this->dbMetaDataTableName = $this->dbTableName . '_md';
$this->dbForeignKeyName = $dbPrefix . '_id';
- $this->cuType = $cuType;
}
use PrincipalProxyTrait;
@@ -110,8 +86,8 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
$metaDataById[$metaDataRow[$this->dbForeignKeyName]] = [];
}
- $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] =
- $metaDataRow['value'];
+ $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']]
+ = $metaDataRow['value'];
}
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
@@ -386,7 +362,7 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
try {
$stmt = $query->executeQuery();
} catch (Exception $e) {
- $this->logger->error("Could not search resources: " . $e->getMessage(), ['exception' => $e]);
+ $this->logger->error('Could not search resources: ' . $e->getMessage(), ['exception' => $e]);
}
$rows = [];
@@ -495,9 +471,9 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
* @return bool
*/
private function isAllowedToAccessResource(array $row, array $userGroups): bool {
- if (!isset($row['group_restrictions']) ||
- $row['group_restrictions'] === null ||
- $row['group_restrictions'] === '') {
+ if (!isset($row['group_restrictions'])
+ || $row['group_restrictions'] === null
+ || $row['group_restrictions'] === '') {
return true;
}
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
index 40396f67ce9..c70d93daf52 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
index 91cf78c296f..5704b23ae14 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/RetentionService.php b/apps/dav/lib/CalDAV/RetentionService.php
index 72301927f2b..399d1a46639 100644
--- a/apps/dav/lib/CalDAV/RetentionService.php
+++ b/apps/dav/lib/CalDAV/RetentionService.php
@@ -17,29 +17,19 @@ class RetentionService {
public const RETENTION_CONFIG_KEY = 'calendarRetentionObligation';
private const DEFAULT_RETENTION_SECONDS = 30 * 24 * 60 * 60;
- /** @var IConfig */
- private $config;
-
- /** @var ITimeFactory */
- private $time;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- public function __construct(IConfig $config,
- ITimeFactory $time,
- CalDavBackend $calDavBackend) {
- $this->config = $config;
- $this->time = $time;
- $this->calDavBackend = $calDavBackend;
+ public function __construct(
+ private IConfig $config,
+ private ITimeFactory $time,
+ private CalDavBackend $calDavBackend,
+ ) {
}
public function getDuration(): int {
return max(
- (int) $this->config->getAppValue(
+ (int)$this->config->getAppValue(
Application::APP_ID,
self::RETENTION_CONFIG_KEY,
- (string) self::DEFAULT_RETENTION_SECONDS
+ (string)self::DEFAULT_RETENTION_SECONDS
),
0 // Just making sure we don't delete things in the future when a negative number is passed
);
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
index ef506d1593c..2af6b162d8d 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
@@ -12,9 +12,13 @@ use OCA\DAV\CalDAV\CalendarObject;
use OCA\DAV\CalDAV\EventComparisonService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
-use OCP\IConfig;
+use OCP\IAppConfig;
use OCP\IUserSession;
use OCP\Mail\IMailer;
+use OCP\Mail\Provider\Address;
+use OCP\Mail\Provider\Attachment;
+use OCP\Mail\Provider\IManager as IMailManager;
+use OCP\Mail\Provider\IMessageSend;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin;
@@ -41,38 +45,26 @@ use Sabre\VObject\Reader;
* @license http://sabre.io/license/ Modified BSD License
*/
class IMipPlugin extends SabreIMipPlugin {
- private IUserSession $userSession;
- private IConfig $config;
- private IMailer $mailer;
- private LoggerInterface $logger;
- private ITimeFactory $timeFactory;
- private Defaults $defaults;
+
private ?VCalendar $vCalendar = null;
- private IMipService $imipService;
public const MAX_DATE = '2038-01-01';
public const METHOD_REQUEST = 'request';
public const METHOD_REPLY = 'reply';
public const METHOD_CANCEL = 'cancel';
- public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
- private EventComparisonService $eventComparisonService;
-
- public function __construct(IConfig $config,
- IMailer $mailer,
- LoggerInterface $logger,
- ITimeFactory $timeFactory,
- Defaults $defaults,
- IUserSession $userSession,
- IMipService $imipService,
- EventComparisonService $eventComparisonService) {
+ public const IMIP_INDENT = 15;
+
+ public function __construct(
+ private IAppConfig $config,
+ private IMailer $mailer,
+ private LoggerInterface $logger,
+ private ITimeFactory $timeFactory,
+ private Defaults $defaults,
+ private IUserSession $userSession,
+ private IMipService $imipService,
+ private EventComparisonService $eventComparisonService,
+ private IMailManager $mailManager,
+ ) {
parent::__construct('');
- $this->userSession = $userSession;
- $this->config = $config;
- $this->mailer = $mailer;
- $this->logger = $logger;
- $this->timeFactory = $timeFactory;
- $this->defaults = $defaults;
- $this->imipService = $imipService;
- $this->eventComparisonService = $eventComparisonService;
}
public function initialize(DAV\Server $server): void {
@@ -89,7 +81,7 @@ class IMipPlugin extends SabreIMipPlugin {
* @param bool $modified modified
*/
public function beforeWriteContent($uri, INode $node, $data, $modified): void {
- if(!$node instanceof CalendarObject) {
+ if (!$node instanceof CalendarObject) {
return;
}
/** @var VCalendar $vCalendar */
@@ -104,8 +96,8 @@ class IMipPlugin extends SabreIMipPlugin {
* @return void
*/
public function schedule(Message $iTipMessage) {
- // Not sending any emails if the system considers the update
- // insignificant.
+
+ // Not sending any emails if the system considers the update insignificant
if (!$iTipMessage->significantChange) {
if (!$iTipMessage->scheduleStatus) {
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
@@ -146,7 +138,7 @@ class IMipPlugin extends SabreIMipPlugin {
// No changed events after all - this shouldn't happen if there is significant change yet here we are
// The scheduling status is debatable
- if(empty($vEvent)) {
+ if (empty($vEvent)) {
$this->logger->warning('iTip message said the change was significant but comparison did not detect any updated VEvents');
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
return;
@@ -158,15 +150,16 @@ class IMipPlugin extends SabreIMipPlugin {
// we also might not have an old event as this could be a new
// invitation, or a new recurrence exception
$attendee = $this->imipService->getCurrentAttendee($iTipMessage);
- if($attendee === null) {
+ if ($attendee === null) {
$uid = $vEvent->UID ?? 'no UID found';
$this->logger->debug('Could not find recipient ' . $recipient . ' as attendee for event with UID ' . $uid);
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
return;
}
- // Don't send emails to things
- if($this->imipService->isRoomOrResource($attendee)) {
- $this->logger->debug('No invitation sent as recipient is room or resource', [
+ // Don't send emails to rooms, resources and circles
+ if ($this->imipService->isRoomOrResource($attendee)
+ || $this->imipService->isCircle($attendee)) {
+ $this->logger->debug('No invitation sent as recipient is room, resource or circle', [
'attendee' => $recipient,
]);
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
@@ -193,7 +186,7 @@ class IMipPlugin extends SabreIMipPlugin {
switch (strtolower($iTipMessage->method)) {
case self::METHOD_REPLY:
$method = self::METHOD_REPLY;
- $data = $this->imipService->buildBodyData($vEvent, $oldVevent);
+ $data = $this->imipService->buildReplyBodyData($vEvent);
$replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage);
break;
case self::METHOD_CANCEL:
@@ -212,21 +205,6 @@ class IMipPlugin extends SabreIMipPlugin {
$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
$fromName = $this->imipService->getFrom($senderName, $this->defaults->getName());
- $message = $this->mailer->createMessage()
- ->setFrom([$fromEMail => $fromName]);
-
- if ($recipientName !== null) {
- $message->setTo([$recipient => $recipientName]);
- } else {
- $message->setTo([$recipient]);
- }
-
- if ($senderName !== null) {
- $message->setReplyTo([$sender => $senderName]);
- } else {
- $message->setReplyTo([$sender]);
- }
-
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
$template->addHeader();
@@ -256,7 +234,7 @@ class IMipPlugin extends SabreIMipPlugin {
*/
$recipientDomain = substr(strrchr($recipient, '@'), 1);
- $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
+ $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getValueString('dav', 'invitation_link_recipients', 'yes'))));
if (strcmp('yes', $invitationLinkRecipients[0]) === 0
|| in_array(strtolower($recipient), $invitationLinkRecipients)
@@ -268,18 +246,70 @@ class IMipPlugin extends SabreIMipPlugin {
}
$template->addFooter();
-
- $message->useTemplate($template);
-
+ // convert iTip Message to string
$itip_msg = $iTipMessage->message->serialize();
- $message->attachInline(
- $itip_msg,
- 'event.ics',
- 'text/calendar; method=' . $iTipMessage->method,
- );
+
+ $mailService = null;
try {
- $failed = $this->mailer->send($message);
+ if ($this->config->getValueBool('core', 'mail_providers_enabled', true)) {
+ // retrieve user object
+ $user = $this->userSession->getUser();
+ if ($user !== null) {
+ // retrieve appropriate service with the same address as sender
+ $mailService = $this->mailManager->findServiceByAddress($user->getUID(), $sender);
+ }
+ }
+
+ // The display name in Nextcloud can use utf-8.
+ // As the default charset for text/* is us-ascii, it's important to explicitly define it.
+ // See https://www.rfc-editor.org/rfc/rfc6047.html#section-2.4.
+ $contentType = 'text/calendar; method=' . $iTipMessage->method . '; charset="utf-8"';
+
+ // evaluate if a mail service was found and has sending capabilities
+ if ($mailService instanceof IMessageSend) {
+ // construct mail message and set required parameters
+ $message = $mailService->initiateMessage();
+ $message->setFrom(
+ (new Address($sender, $fromName))
+ );
+ $message->setTo(
+ (new Address($recipient, $recipientName))
+ );
+ $message->setSubject($template->renderSubject());
+ $message->setBodyPlain($template->renderText());
+ $message->setBodyHtml($template->renderHtml());
+ // Adding name=event.ics is a trick to make the invitation also appear
+ // as a file attachment in mail clients like Thunderbird or Evolution.
+ $message->setAttachments((new Attachment(
+ $itip_msg,
+ null,
+ $contentType . '; name=event.ics',
+ true
+ )));
+ // send message
+ $mailService->sendMessage($message);
+ } else {
+ // construct symfony mailer message and set required parameters
+ $message = $this->mailer->createMessage();
+ $message->setFrom([$fromEMail => $fromName]);
+ $message->setTo(
+ (($recipientName !== null) ? [$recipient => $recipientName] : [$recipient])
+ );
+ $message->setReplyTo(
+ (($senderName !== null) ? [$sender => $senderName] : [$sender])
+ );
+ $message->useTemplate($template);
+ // Using a different content type because Symfony Mailer/Mime will append the name to
+ // the content type header and attachInline does not allow null.
+ $message->attachInline(
+ $itip_msg,
+ 'event.ics',
+ $contentType,
+ );
+ $failed = $this->mailer->send($message);
+ }
+
$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
if (!empty($failed)) {
$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php
index ec5e4f4d1ac..54c0bc31849 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipService.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php
@@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Schedule;
use OC\URLGenerator;
+use OCA\DAV\CalDAV\EventReader;
+use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
@@ -25,11 +27,6 @@ use Sabre\VObject\Recur\EventIterator;
class IMipService {
- private URLGenerator $urlGenerator;
- private IConfig $config;
- private IDBConnection $db;
- private ISecureRandom $random;
- private L10NFactory $l10nFactory;
private IL10N $l10n;
/** @var string[] */
@@ -40,18 +37,17 @@ class IMipService {
'meeting_location' => 'LOCATION'
];
- public function __construct(URLGenerator $urlGenerator,
- IConfig $config,
- IDBConnection $db,
- ISecureRandom $random,
- L10NFactory $l10nFactory) {
- $this->urlGenerator = $urlGenerator;
- $this->config = $config;
- $this->db = $db;
- $this->random = $random;
- $this->l10nFactory = $l10nFactory;
- $default = $this->l10nFactory->findGenericLanguage();
- $this->l10n = $this->l10nFactory->get('dav', $default);
+ public function __construct(
+ private URLGenerator $urlGenerator,
+ private IConfig $config,
+ private IDBConnection $db,
+ private ISecureRandom $random,
+ private L10NFactory $l10nFactory,
+ private ITimeFactory $timeFactory,
+ ) {
+ $language = $this->l10nFactory->findGenericLanguage();
+ $locale = $this->l10nFactory->findLocale($language);
+ $this->l10n = $this->l10nFactory->get('dav', $language, $locale);
}
/**
@@ -83,7 +79,7 @@ class IMipService {
return $default;
}
$newstring = $vevent->$property->getValue();
- if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
+ if (isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
$oldstring = $oldVEvent->$property->getValue();
return sprintf($strikethrough, $oldstring, $newstring);
}
@@ -130,11 +126,15 @@ class IMipService {
* @return array
*/
public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
+
+ // construct event reader
+ $eventReaderCurrent = new EventReader($vEvent);
+ $eventReaderPrevious = !empty($oldVEvent) ? new EventReader($oldVEvent) : null;
$defaultVal = '';
$data = [];
- $data['meeting_when'] = $this->generateWhenString($vEvent);
+ $data['meeting_when'] = $this->generateWhenString($eventReaderCurrent);
- foreach(self::STRING_DIFF as $key => $property) {
+ foreach (self::STRING_DIFF as $key => $property) {
$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
}
@@ -144,8 +144,8 @@ class IMipService {
$data['meeting_location_html'] = $locationHtml;
}
- if(!empty($oldVEvent)) {
- $oldMeetingWhen = $this->generateWhenString($oldVEvent);
+ if (!empty($oldVEvent)) {
+ $oldMeetingWhen = $this->generateWhenString($eventReaderPrevious);
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
$data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
@@ -153,107 +153,635 @@ class IMipService {
$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
- $data['meeting_when_html'] =
- ($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null)
- ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when'])
- : $data['meeting_when'];
+ $data['meeting_when_html'] = $oldMeetingWhen !== $data['meeting_when'] ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when']) : $data['meeting_when'];
+ }
+ // generate occurring next string
+ if ($eventReaderCurrent->recurs()) {
+ $data['meeting_occurring'] = $this->generateOccurringString($eventReaderCurrent);
}
return $data;
}
/**
- * @param IL10N $this->l10n
- * @param VEvent $vevent
- * @return false|int|string
+ * @param VEvent $vEvent
+ * @return array
*/
- public function generateWhenString(VEvent $vevent) {
- /** @var Property\ICalendar\DateTime $dtstart */
- $dtstart = $vevent->DTSTART;
- if (isset($vevent->DTEND)) {
- /** @var Property\ICalendar\DateTime $dtend */
- $dtend = $vevent->DTEND;
- } elseif (isset($vevent->DURATION)) {
- $isFloating = $dtstart->isFloating();
- $dtend = clone $dtstart;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
- $dtend->setDateTime($endDateTime, $isFloating);
- } elseif (!$dtstart->hasTime()) {
- $isFloating = $dtstart->isFloating();
- $dtend = clone $dtstart;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->modify('+1 day');
- $dtend->setDateTime($endDateTime, $isFloating);
- } else {
- $dtend = clone $dtstart;
+ public function buildReplyBodyData(VEvent $vEvent): array {
+ // construct event reader
+ $eventReader = new EventReader($vEvent);
+ $defaultVal = '';
+ $data = [];
+ $data['meeting_when'] = $this->generateWhenString($eventReader);
+
+ foreach (self::STRING_DIFF as $key => $property) {
+ $data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
}
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
- /** @var \DateTimeImmutable $dtstartDt */
- $dtstartDt = $dtstart->getDateTime();
+ if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
+ $data['meeting_location_html'] = $locationHtml;
+ }
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
- /** @var \DateTimeImmutable $dtendDt */
- $dtendDt = $dtend->getDateTime();
+ $data['meeting_url_html'] = $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $data['meeting_url']) : '';
- $diff = $dtstartDt->diff($dtendDt);
+ // generate occurring next string
+ if ($eventReader->recurs()) {
+ $data['meeting_occurring'] = $this->generateOccurringString($eventReader);
+ }
- $dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
- $dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
+ return $data;
+ }
- if ($dtstart instanceof Property\ICalendar\Date) {
- // One day event
- if ($diff->days === 1) {
- return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
- }
+ /**
+ * generates a when string based on if a event has an recurrence or not
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenString(EventReader $er): string {
+ return match ($er->recurs()) {
+ true => $this->generateWhenStringRecurring($er),
+ false => $this->generateWhenStringSingular($er)
+ };
+ }
- // DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
- // the email should show 2020-01-01 to 2020-01-04.
- $dtendDt->modify('-1 day');
+ /**
+ * generates a when string for a non recurring event
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringSingular(EventReader $er): string {
+ // initialize
+ $startTime = null;
+ $endTime = null;
+ // calculate time difference from now to start of event
+ $occurring = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
+ // extract start date
+ $startDate = $this->l10n->l('date', $er->startDateTime(), ['width' => 'full']);
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // In a minute/hour/day/week/month/year on July 1, 2024 for the entire day
+ // In a minute/hour/day/week/month/year on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 for the entire day
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 between 8:00 AM - 9:00 AM (America/Toronto)
+ return match ([$occurring['scale'], $endTime !== null]) {
+ ['past', false] => $this->l10n->t(
+ 'In the past on %1$s for the entire day',
+ [$startDate]
+ ),
+ ['minute', false] => $this->l10n->n(
+ 'In a minute on %1$s for the entire day',
+ 'In %n minutes on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['hour', false] => $this->l10n->n(
+ 'In a hour on %1$s for the entire day',
+ 'In %n hours on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['day', false] => $this->l10n->n(
+ 'In a day on %1$s for the entire day',
+ 'In %n days on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['week', false] => $this->l10n->n(
+ 'In a week on %1$s for the entire day',
+ 'In %n weeks on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['month', false] => $this->l10n->n(
+ 'In a month on %1$s for the entire day',
+ 'In %n months on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['year', false] => $this->l10n->n(
+ 'In a year on %1$s for the entire day',
+ 'In %n years on %1$s for the entire day',
+ $occurring['interval'],
+ [$startDate]
+ ),
+ ['past', true] => $this->l10n->t(
+ 'In the past on %1$s between %2$s - %3$s',
+ [$startDate, $startTime, $endTime]
+ ),
+ ['minute', true] => $this->l10n->n(
+ 'In a minute on %1$s between %2$s - %3$s',
+ 'In %n minutes on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['hour', true] => $this->l10n->n(
+ 'In a hour on %1$s between %2$s - %3$s',
+ 'In %n hours on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['day', true] => $this->l10n->n(
+ 'In a day on %1$s between %2$s - %3$s',
+ 'In %n days on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['week', true] => $this->l10n->n(
+ 'In a week on %1$s between %2$s - %3$s',
+ 'In %n weeks on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['month', true] => $this->l10n->n(
+ 'In a month on %1$s between %2$s - %3$s',
+ 'In %n months on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ ['year', true] => $this->l10n->n(
+ 'In a year on %1$s between %2$s - %3$s',
+ 'In %n years on %1$s between %2$s - %3$s',
+ $occurring['interval'],
+ [$startDate, $startTime, $endTime]
+ ),
+ default => $this->l10n->t('Could not generate when statement')
+ };
+ }
- //event that spans over multiple days
- $localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
- $localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
+ /**
+ * generates a when string based on recurrence precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurring(EventReader $er): string {
+ return match ($er->recurringPrecision()) {
+ 'daily' => $this->generateWhenStringRecurringDaily($er),
+ 'weekly' => $this->generateWhenStringRecurringWeekly($er),
+ 'monthly' => $this->generateWhenStringRecurringMonthly($er),
+ 'yearly' => $this->generateWhenStringRecurringYearly($er),
+ 'fixed' => $this->generateWhenStringRecurringFixed($er),
+ };
+ }
- return $localeStart . ' - ' . $localeEnd;
+ /**
+ * generates a when string for a daily precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringDaily(EventReader $er): string {
+
+ // initialize
+ $interval = (int)$er->recurringInterval();
+ $startTime = null;
+ $conclusion = null;
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
}
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // Every Day for the entire day
+ // Every Day for the entire day until July 13, 2024
+ // Every Day between 8:00 AM - 9:00 AM (America/Toronto)
+ // Every Day between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ // Every 3 Days for the entire day
+ // Every 3 Days for the entire day until July 13, 2024
+ // Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto)
+ // Every 3 Days between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ return match ([($interval > 1), $startTime !== null, $conclusion !== null]) {
+ [false, false, false] => $this->l10n->t('Every Day for the entire day'),
+ [false, false, true] => $this->l10n->t('Every Day for the entire day until %1$s', [$conclusion]),
+ [false, true, false] => $this->l10n->t('Every Day between %1$s - %2$s', [$startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Day between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Days for the entire day', [$interval]),
+ [true, false, true] => $this->l10n->t('Every %1$d Days for the entire day until %2$s', [$interval, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Days between %2$s - %3$s', [$interval, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Days between %2$s - %3$s until %4$s', [$interval, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
- /** @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();
- }
+ /**
+ * generates a when string for a weekly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringWeekly(EventReader $er): string {
+
+ // initialize
+ $interval = (int)$er->recurringInterval();
+ $startTime = null;
+ $conclusion = null;
+ // days of the week
+ $days = implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // Every Week on Monday, Wednesday, Friday for the entire day
+ // Every Week on Monday, Wednesday, Friday for the entire day until July 13, 2024
+ // Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Every Week on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ // Every 2 Weeks on Monday, Wednesday, Friday for the entire day
+ // Every 2 Weeks on Monday, Wednesday, Friday for the entire day until July 13, 2024
+ // Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Every 2 Weeks on Monday, Wednesday, Friday between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ return match ([($interval > 1), $startTime !== null, $conclusion !== null]) {
+ [false, false, false] => $this->l10n->t('Every Week on %1$s for the entire day', [$days]),
+ [false, false, true] => $this->l10n->t('Every Week on %1$s for the entire day until %2$s', [$days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s', [$days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day', [$interval, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day until %3$s', [$interval, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
- $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 . ')';
+ /**
+ * generates a when string for a monthly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringMonthly(EventReader $er): string {
+
+ // initialize
+ $interval = (int)$er->recurringInterval();
+ $startTime = null;
+ $conclusion = null;
+ // days of month
+ if ($er->recurringPattern() === 'R') {
+ $days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' '
+ . implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
+ } else {
+ $days = implode(', ', $er->recurringDaysOfMonth());
+ }
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
}
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order, output varies depending on if the event is absolute or releative:
+ // Absolute: Every Month on the 1, 8 for the entire day
+ // Relative: Every Month on the First Sunday, Saturday for the entire day
+ // Absolute: Every Month on the 1, 8 for the entire day until December 31, 2024
+ // Relative: Every Month on the First Sunday, Saturday for the entire day until December 31, 2024
+ // Absolute: Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)
+ // Relative: Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Absolute: Every Month on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024
+ // Relative: Every Month on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024
+ // Absolute: Every 2 Months on the 1, 8 for the entire day
+ // Relative: Every 2 Months on the First Sunday, Saturday for the entire day
+ // Absolute: Every 2 Months on the 1, 8 for the entire day until December 31, 2024
+ // Relative: Every 2 Months on the First Sunday, Saturday for the entire day until December 31, 2024
+ // Absolute: Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto)
+ // Relative: Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Absolute: Every 2 Months on the 1, 8 between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024
+ // Relative: Every 2 Months on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until December 31, 2024
+ return match ([($interval > 1), $startTime !== null, $conclusion !== null]) {
+ [false, false, false] => $this->l10n->t('Every Month on the %1$s for the entire day', [$days]),
+ [false, false, true] => $this->l10n->t('Every Month on the %1$s for the entire day until %2$s', [$days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s', [$days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day', [$interval, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day until %3$s', [$interval, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
+ }
- // show only end time if date is the same
- if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
- $localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
+ /**
+ * generates a when string for a yearly precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringYearly(EventReader $er): string {
+
+ // initialize
+ $interval = (int)$er->recurringInterval();
+ $startTime = null;
+ $conclusion = null;
+ // months of year
+ $months = implode(', ', array_map(function ($value) { return $this->localizeMonthName($value); }, $er->recurringMonthsOfYearNamed()));
+ // days of month
+ if ($er->recurringPattern() === 'R') {
+ $days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' '
+ . implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
} else {
- $localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
- $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
+ $days = $er->startDateTime()->format('jS');
+ }
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ if ($er->recurringConcludes()) {
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ }
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order, output varies depending on if the event is absolute or releative:
+ // Absolute: Every Year in July on the 1st for the entire day
+ // Relative: Every Year in July on the First Sunday, Saturday for the entire day
+ // Absolute: Every Year in July on the 1st for the entire day until July 31, 2026
+ // Relative: Every Year in July on the First Sunday, Saturday for the entire day until July 31, 2026
+ // Absolute: Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)
+ // Relative: Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Absolute: Every Year in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026
+ // Relative: Every Year in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026
+ // Absolute: Every 2 Years in July on the 1st for the entire day
+ // Relative: Every 2 Years in July on the First Sunday, Saturday for the entire day
+ // Absolute: Every 2 Years in July on the 1st for the entire day until July 31, 2026
+ // Relative: Every 2 Years in July on the First Sunday, Saturday for the entire day until July 31, 2026
+ // Absolute: Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto)
+ // Relative: Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto)
+ // Absolute: Every 2 Years in July on the 1st between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026
+ // Relative: Every 2 Years in July on the First Sunday, Saturday between 8:00 AM - 9:00 AM (America/Toronto) until July 31, 2026
+ return match ([($interval > 1), $startTime !== null, $conclusion !== null]) {
+ [false, false, false] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day', [$months, $days]),
+ [false, false, true] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day until %3$s', [$months, $days, $conclusion]),
+ [false, true, false] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s', [$months, $days, $startTime, $endTime]),
+ [false, true, true] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', [$months, $days, $startTime, $endTime, $conclusion]),
+ [true, false, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day', [$interval, $months, $days]),
+ [true, false, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [$interval, $months, $days, $conclusion]),
+ [true, true, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [$interval, $months, $days, $startTime, $endTime]),
+ [true, true, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [$interval, $months, $days, $startTime, $endTime, $conclusion]),
+ default => $this->l10n->t('Could not generate event recurrence statement')
+ };
+ }
+
+ /**
+ * generates a when string for a fixed precision/frequency
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateWhenStringRecurringFixed(EventReader $er): string {
+ // initialize
+ $startTime = null;
+ $conclusion = null;
+ // time of the day
+ if (!$er->entireDay()) {
+ $startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
+ $startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
+ $endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
+ }
+ // conclusion
+ $conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // On specific dates for the entire day until July 13, 2024
+ // On specific dates between 8:00 AM - 9:00 AM (America/Toronto) until July 13, 2024
+ return match ($startTime !== null) {
+ false => $this->l10n->t('On specific dates for the entire day until %1$s', [$conclusion]),
+ true => $this->l10n->t('On specific dates between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]),
+ };
+ }
+
+ /**
+ * generates a occurring next string for a recurring event
+ *
+ * @since 30.0.0
+ *
+ * @param EventReader $er
+ *
+ * @return string
+ */
+ public function generateOccurringString(EventReader $er): string {
+
+ // initialize
+ $occurrence = null;
+ $occurrence2 = null;
+ $occurrence3 = null;
+ // reset to initial occurrence
+ $er->recurrenceRewind();
+ // forward to current date
+ $er->recurrenceAdvanceTo($this->timeFactory->getDateTime());
+ // calculate time difference from now to start of next event occurrence and minimize it
+ $occurrenceIn = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
+ // store next occurrence value
+ $occurrence = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+ // forward one occurrence
+ $er->recurrenceAdvance();
+ // evaluate if occurrence is valid
+ if ($er->recurrenceDate() !== null) {
+ // store following occurrence value
+ $occurrence2 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+ // forward one occurrence
+ $er->recurrenceAdvance();
+ // evaluate if occurrence is valid
+ if ($er->recurrenceDate()) {
+ // store following occurrence value
+ $occurrence3 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
+ }
}
+ // generate localized when string
+ // TRANSLATORS
+ // Indicates when a calendar event will happen, shown on invitation emails
+ // Output produced in order:
+ // In a minute/hour/day/week/month/year on July 1, 2024
+ // In a minute/hour/day/week/month/year on July 1, 2024 then on July 3, 2024
+ // In a minute/hour/day/week/month/year on July 1, 2024 then on July 3, 2024 and July 5, 2024
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 then on July 3, 2024
+ // In 2 minutes/hours/days/weeks/months/years on July 1, 2024 then on July 3, 2024 and July 5, 2024
+ return match ([$occurrenceIn['scale'], $occurrence2 !== null, $occurrence3 !== null]) {
+ ['past', false, false] => $this->l10n->t(
+ 'In the past on %1$s',
+ [$occurrence]
+ ),
+ ['minute', false, false] => $this->l10n->n(
+ 'In a minute on %1$s',
+ 'In %n minutes on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['hour', false, false] => $this->l10n->n(
+ 'In a hour on %1$s',
+ 'In %n hours on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['day', false, false] => $this->l10n->n(
+ 'In a day on %1$s',
+ 'In %n days on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['week', false, false] => $this->l10n->n(
+ 'In a week on %1$s',
+ 'In %n weeks on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['month', false, false] => $this->l10n->n(
+ 'In a month on %1$s',
+ 'In %n months on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['year', false, false] => $this->l10n->n(
+ 'In a year on %1$s',
+ 'In %n years on %1$s',
+ $occurrenceIn['interval'],
+ [$occurrence]
+ ),
+ ['past', true, false] => $this->l10n->t(
+ 'In the past on %1$s then on %2$s',
+ [$occurrence, $occurrence2]
+ ),
+ ['minute', true, false] => $this->l10n->n(
+ 'In a minute on %1$s then on %2$s',
+ 'In %n minutes on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['hour', true, false] => $this->l10n->n(
+ 'In a hour on %1$s then on %2$s',
+ 'In %n hours on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['day', true, false] => $this->l10n->n(
+ 'In a day on %1$s then on %2$s',
+ 'In %n days on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['week', true, false] => $this->l10n->n(
+ 'In a week on %1$s then on %2$s',
+ 'In %n weeks on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['month', true, false] => $this->l10n->n(
+ 'In a month on %1$s then on %2$s',
+ 'In %n months on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['year', true, false] => $this->l10n->n(
+ 'In a year on %1$s then on %2$s',
+ 'In %n years on %1$s then on %2$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2]
+ ),
+ ['past', true, true] => $this->l10n->t(
+ 'In the past on %1$s then on %2$s and %3$s',
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['minute', true, true] => $this->l10n->n(
+ 'In a minute on %1$s then on %2$s and %3$s',
+ 'In %n minutes on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['hour', true, true] => $this->l10n->n(
+ 'In a hour on %1$s then on %2$s and %3$s',
+ 'In %n hours on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['day', true, true] => $this->l10n->n(
+ 'In a day on %1$s then on %2$s and %3$s',
+ 'In %n days on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['week', true, true] => $this->l10n->n(
+ 'In a week on %1$s then on %2$s and %3$s',
+ 'In %n weeks on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['month', true, true] => $this->l10n->n(
+ 'In a month on %1$s then on %2$s and %3$s',
+ 'In %n months on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ ['year', true, true] => $this->l10n->n(
+ 'In a year on %1$s then on %2$s and %3$s',
+ 'In %n years on %1$s then on %2$s and %3$s',
+ $occurrenceIn['interval'],
+ [$occurrence, $occurrence2, $occurrence3]
+ ),
+ default => $this->l10n->t('Could not generate next recurrence statement')
+ };
- return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
}
/**
@@ -261,12 +789,13 @@ class IMipService {
* @return array
*/
public function buildCancelledBodyData(VEvent $vEvent): array {
+ // construct event reader
+ $eventReaderCurrent = new EventReader($vEvent);
$defaultVal = '';
$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
- $newMeetingWhen = $this->generateWhenString($vEvent);
+ $newMeetingWhen = $this->generateWhenString($eventReaderCurrent);
$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');
- ;
$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
@@ -320,7 +849,7 @@ class IMipService {
return $dtEnd->getDateTime()->getTimeStamp();
}
- if(isset($component->DURATION)) {
+ if (isset($component->DURATION)) {
/** @var \DateTime $endDate */
$endDate = clone $dtStart->getDateTime();
// $component->DTEND->getDateTime() returns DateTimeImmutable
@@ -328,7 +857,7 @@ class IMipService {
return $endDate->getTimestamp();
}
- if(!$dtStart->hasTime()) {
+ if (!$dtStart->hasTime()) {
/** @var \DateTime $endDate */
// $component->DTSTART->getDateTime() returns DateTimeImmutable
$endDate = clone $dtStart->getDateTime();
@@ -344,7 +873,7 @@ class IMipService {
* @param Property|null $attendee
*/
public function setL10n(?Property $attendee = null) {
- if($attendee === null) {
+ if ($attendee === null) {
return;
}
@@ -360,7 +889,7 @@ class IMipService {
* @return bool
*/
public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
- if($attendee === null) {
+ if ($attendee === null) {
return false;
}
@@ -468,10 +997,10 @@ class IMipService {
htmlspecialchars($organizer->getNormalizedValue()),
htmlspecialchars($organizerName ?: $organizerEmail));
$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
- if(isset($organizer['PARTSTAT'])) {
+ if (isset($organizer['PARTSTAT'])) {
/** @var Parameter $partstat */
$partstat = $organizer['PARTSTAT'];
- if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
+ if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
$organizerHTML .= ' ✔︎';
$organizerText .= ' ✔︎';
}
@@ -522,7 +1051,7 @@ class IMipService {
$data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'),
$this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT);
if ($data['meeting_when'] !== '') {
- $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Date and time:'),
+ $template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('When:'),
$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
}
if ($data['meeting_location'] !== '') {
@@ -533,6 +1062,10 @@ class IMipService {
$template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'),
$this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT);
}
+ if (isset($data['meeting_occurring'])) {
+ $template->addBodyListItem($data['meeting_occurring_html'] ?? $data['meeting_occurring'], $this->l10n->t('Occurring:'),
+ $this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_occurring'], '', IMipPlugin::IMIP_INDENT);
+ }
$this->addAttendees($template, $vevent);
@@ -575,9 +1108,9 @@ class IMipService {
$attendee = $iTipMessage->recipient;
$organizer = $iTipMessage->sender;
$sequence = $iTipMessage->sequence;
- $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
- $vevent->{'RECURRENCE-ID'}->serialize() : null;
- $uid = $vevent->{'UID'};
+ $recurrenceId = isset($vevent->{'RECURRENCE-ID'})
+ ? $vevent->{'RECURRENCE-ID'}->serialize() : null;
+ $uid = $vevent->{'UID'}?->getValue();
$query = $this->db->getQueryBuilder();
$query->insert('calendar_invitations')
@@ -590,37 +1123,12 @@ class IMipService {
'expiration' => $query->createNamedParameter($lastOccurrence),
'uid' => $query->createNamedParameter($uid)
])
- ->execute();
+ ->executeStatement();
return $token;
}
/**
- * Create a valid VCalendar object out of the details of
- * a VEvent and its associated iTip Message
- *
- * We do this to filter out all unchanged VEvents
- * This is especially important in iTip Messages with recurrences
- * and recurrence exceptions
- *
- * @param Message $iTipMessage
- * @param VEvent $vEvent
- * @return VCalendar
- */
- public function generateVCalendar(Message $iTipMessage, VEvent $vEvent): VCalendar {
- $vCalendar = new VCalendar();
- $vCalendar->add('METHOD', $iTipMessage->method);
- foreach ($iTipMessage->message->getComponents() as $component) {
- if ($component instanceof VEvent) {
- continue;
- }
- $vCalendar->add(clone $component);
- }
- $vCalendar->add($vEvent);
- return $vCalendar;
- }
-
- /**
* @param IEMailTemplate $template
* @param $token
*/
@@ -664,7 +1172,7 @@ class IMipService {
public function isRoomOrResource(Property $attendee): bool {
$cuType = $attendee->offsetGet('CUTYPE');
- if(!$cuType instanceof Parameter) {
+ if (!$cuType instanceof Parameter) {
return false;
}
$type = $cuType->getValue() ?? 'INDIVIDUAL';
@@ -674,4 +1182,113 @@ class IMipService {
}
return false;
}
+
+ public function isCircle(Property $attendee): bool {
+ $cuType = $attendee->offsetGet('CUTYPE');
+ if (!$cuType instanceof Parameter) {
+ return false;
+ }
+
+ $uri = $attendee->getValue();
+ if (!$uri) {
+ return false;
+ }
+
+ $cuTypeValue = $cuType->getValue();
+ return $cuTypeValue === 'GROUP' && str_starts_with($uri, 'mailto:circle+');
+ }
+
+ public function minimizeInterval(\DateInterval $dateInterval): array {
+ // evaluate if time interval is in the past
+ if ($dateInterval->invert == 1) {
+ return ['interval' => 1, 'scale' => 'past'];
+ }
+ // evaluate interval parts and return smallest time period
+ if ($dateInterval->y > 0) {
+ $interval = $dateInterval->y;
+ $scale = 'year';
+ } elseif ($dateInterval->m > 0) {
+ $interval = $dateInterval->m;
+ $scale = 'month';
+ } elseif ($dateInterval->d >= 7) {
+ $interval = (int)($dateInterval->d / 7);
+ $scale = 'week';
+ } elseif ($dateInterval->d > 0) {
+ $interval = $dateInterval->d;
+ $scale = 'day';
+ } elseif ($dateInterval->h > 0) {
+ $interval = $dateInterval->h;
+ $scale = 'hour';
+ } else {
+ $interval = $dateInterval->i;
+ $scale = 'minute';
+ }
+
+ return ['interval' => $interval, 'scale' => $scale];
+ }
+
+ /**
+ * Localizes week day names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeDayName(string $value): string {
+ return match ($value) {
+ 'Monday' => $this->l10n->t('Monday'),
+ 'Tuesday' => $this->l10n->t('Tuesday'),
+ 'Wednesday' => $this->l10n->t('Wednesday'),
+ 'Thursday' => $this->l10n->t('Thursday'),
+ 'Friday' => $this->l10n->t('Friday'),
+ 'Saturday' => $this->l10n->t('Saturday'),
+ 'Sunday' => $this->l10n->t('Sunday'),
+ };
+ }
+
+ /**
+ * Localizes month names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeMonthName(string $value): string {
+ return match ($value) {
+ 'January' => $this->l10n->t('January'),
+ 'February' => $this->l10n->t('February'),
+ 'March' => $this->l10n->t('March'),
+ 'April' => $this->l10n->t('April'),
+ 'May' => $this->l10n->t('May'),
+ 'June' => $this->l10n->t('June'),
+ 'July' => $this->l10n->t('July'),
+ 'August' => $this->l10n->t('August'),
+ 'September' => $this->l10n->t('September'),
+ 'October' => $this->l10n->t('October'),
+ 'November' => $this->l10n->t('November'),
+ 'December' => $this->l10n->t('December'),
+ };
+ }
+
+ /**
+ * Localizes relative position names to another language
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function localizeRelativePositionName(string $value): string {
+ return match ($value) {
+ 'First' => $this->l10n->t('First'),
+ 'Second' => $this->l10n->t('Second'),
+ 'Third' => $this->l10n->t('Third'),
+ 'Fourth' => $this->l10n->t('Fourth'),
+ 'Fifth' => $this->l10n->t('Fifth'),
+ 'Last' => $this->l10n->t('Last'),
+ 'Second Last' => $this->l10n->t('Second Last'),
+ 'Third Last' => $this->l10n->t('Third Last'),
+ 'Fourth Last' => $this->l10n->t('Fourth Last'),
+ 'Fifth Last' => $this->l10n->t('Fifth Last'),
+ };
+ }
}
diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php
index 9e1006e72c0..a001df8b2a8 100644
--- a/apps/dav/lib/CalDAV/Schedule/Plugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -9,11 +10,15 @@ use DateTimeZone;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\TipBroker;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\ICalendar;
use Sabre\CalDAV\ICalendarObject;
use Sabre\CalDAV\Schedule\ISchedulingObject;
+use Sabre\DAV\Exception as DavException;
use Sabre\DAV\INode;
use Sabre\DAV\IProperties;
use Sabre\DAV\PropFind;
@@ -37,11 +42,6 @@ use function Sabre\Uri\split;
class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
- /**
- * @var IConfig
- */
- private $config;
-
/** @var ITip\Message[] */
private $schedulingResponses = [];
@@ -50,14 +50,15 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type';
public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL';
- private LoggerInterface $logger;
/**
* @param IConfig $config
*/
- public function __construct(IConfig $config, LoggerInterface $logger) {
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private DefaultCalendarValidator $defaultCalendarValidator,
+ ) {
}
/**
@@ -81,6 +82,13 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
}
/**
+ * Returns an instance of the iTip\Broker.
+ */
+ protected function createITipBroker(): TipBroker {
+ return new TipBroker();
+ }
+
+ /**
* Allow manual setting of the object change URL
* to support public write
*
@@ -131,6 +139,11 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$result = [];
}
+ // iterate through items and html decode values
+ foreach ($result as $key => $value) {
+ $result[$key] = urldecode($value);
+ }
+
return $result;
}
@@ -149,7 +162,47 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
}
try {
- parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew);
+
+ // Do not generate iTip and iMip messages if scheduling is disabled for this message
+ if ($request->getHeader('x-nc-scheduling') === 'false') {
+ return;
+ }
+
+ if (!$this->scheduleReply($this->server->httpRequest)) {
+ return;
+ }
+
+ /** @var Calendar $calendarNode */
+ $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
+ // extract addresses for owner
+ $addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
+ // determine if request is from a sharee
+ if ($calendarNode->isShared()) {
+ // extract addresses for sharee and add to address collection
+ $addresses = array_merge(
+ $addresses,
+ $this->getAddressesForPrincipal($calendarNode->getPrincipalURI())
+ );
+ }
+ // determine if we are updating a calendar event
+ if (!$isNew) {
+ // retrieve current calendar event node
+ /** @var CalendarObject $currentNode */
+ $currentNode = $this->server->tree->getNodeForPath($request->getPath());
+ // convert calendar event string data to VCalendar object
+ /** @var \Sabre\VObject\Component\VCalendar $currentObject */
+ $currentObject = Reader::read($currentNode->get());
+ } else {
+ $currentObject = null;
+ }
+ // process request
+ $this->processICalendarChange($currentObject, $vCal, $addresses, [], $modified);
+
+ if ($currentObject) {
+ // Destroy circular references so PHP will GC the object.
+ $currentObject->destroy();
+ }
+
} catch (SameOrganizerForAllComponentsException $e) {
$this->handleSameOrganizerException($e, $vCal, $calendarPath);
}
@@ -208,7 +261,7 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
$calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri);
if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) {
- $this->logger->debug('Calendar user type is room or resource, not processing further');
+ $this->logger->debug('Calendar user type is neither room nor resource, not processing further');
return;
}
@@ -319,8 +372,8 @@ EOF;
return null;
}
- $isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources') ||
- str_starts_with($principalUrl, 'principals/calendar-rooms');
+ $isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources')
+ || str_starts_with($principalUrl, 'principals/calendar-rooms');
if (str_starts_with($principalUrl, 'principals/users')) {
[, $userId] = split($principalUrl);
@@ -355,11 +408,20 @@ EOF;
* - isn't a calendar subscription
* - user can write to it (no virtual/3rd-party calendars)
* - calendar isn't a share
+ * - calendar supports VEVENTs
*/
foreach ($calendarHome->getChildren() as $node) {
- if ($node instanceof Calendar && !$node->isSubscription() && $node->canWrite() && !$node->isShared() && !$node->isDeleted()) {
- $userCalendars[] = $node;
+ if (!($node instanceof Calendar)) {
+ continue;
}
+
+ try {
+ $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
+ } catch (DavException $e) {
+ continue;
+ }
+
+ $userCalendars[] = $node;
}
if (count($userCalendars) > 0) {
@@ -368,12 +430,20 @@ EOF;
} else {
// Otherwise if we have really nothing, create a new calendar
if ($currentCalendarDeleted) {
- // If the calendar exists but is deleted, we need to purge it first
- // This may cause some issues in a non synchronous database setup
+ // If the calendar exists but is in the trash bin, we try to rename its uri
+ // so that we can create the new one and still restore the previous one
+ // otherwise we just purge the calendar by removing it before recreating it
$calendar = $this->getCalendar($calendarHome, $uri);
if ($calendar instanceof Calendar) {
- $calendar->disableTrashbin();
- $calendar->delete();
+ $backend = $calendarHome->getCalDAVBackend();
+ if ($backend instanceof CalDavBackend) {
+ // If the CalDAV backend supports moving calendars
+ $this->moveCalendar($backend, $principalUrl, $uri, $uri . '-back-' . time());
+ } else {
+ // Otherwise just purge the calendar
+ $calendar->disableTrashbin();
+ $calendar->delete();
+ }
}
}
$this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
@@ -508,7 +578,9 @@ EOF;
$calendarTimeZone = new DateTimeZone('UTC');
$homePath = $result[0][200]['{' . self::NS_CALDAV . '}calendar-home-set']->getHref();
+ /** @var Calendar $node */
foreach ($this->server->tree->getNodeForPath($homePath)->getChildren() as $node) {
+
if (!$node instanceof ICalendar) {
continue;
}
@@ -640,6 +712,10 @@ EOF;
]);
}
+ private function moveCalendar(CalDavBackend $calDavBackend, string $principalUri, string $oldUri, string $newUri): void {
+ $calDavBackend->moveCalendar($oldUri, $principalUri, $principalUri, $newUri);
+ }
+
/**
* Try to handle the given exception gracefully or throw it if necessary.
*
diff --git a/apps/dav/lib/CalDAV/Search/SearchPlugin.php b/apps/dav/lib/CalDAV/Search/SearchPlugin.php
index fb55dec593c..27e39a76305 100644
--- a/apps/dav/lib/CalDAV/Search/SearchPlugin.php
+++ b/apps/dav/lib/CalDAV/Search/SearchPlugin.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -7,6 +8,7 @@ namespace OCA\DAV\CalDAV\Search;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport;
+use OCP\AppFramework\Http;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
@@ -60,8 +62,8 @@ class SearchPlugin extends ServerPlugin {
$server->on('report', [$this, 'report']);
- $server->xml->elementMap['{' . self::NS_Nextcloud . '}calendar-search'] =
- CalendarSearchReport::class;
+ $server->xml->elementMap['{' . self::NS_Nextcloud . '}calendar-search']
+ = CalendarSearchReport::class;
}
/**
@@ -108,7 +110,7 @@ class SearchPlugin extends ServerPlugin {
* This report is used by clients to request calendar objects based on
* complex conditions.
*
- * @param Xml\Request\CalendarSearchReport $report
+ * @param CalendarSearchReport $report
* @return void
*/
private function calendarSearch($report) {
@@ -133,7 +135,7 @@ class SearchPlugin extends ServerPlugin {
$prefer = $this->server->getHTTPPrefer();
- $this->server->httpResponse->setStatus(207);
+ $this->server->httpResponse->setStatus(Http::STATUS_MULTI_STATUS);
$this->server->httpResponse->setHeader('Content-Type',
'application/xml; charset=utf-8');
$this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php
index 8a130865842..21a4fff1caf 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
index 943e657903e..a98b325397b 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
index 439a795dde9..ef438aa0258 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
index 3b03b63e909..0c31f32348a 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
index 42ecf630f44..251120e35cc 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
index b10cf3140cf..6d6bf958496 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
index 639d0b32655..6ece88fa87b 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
@@ -1,4 +1,5 @@
<?php
+
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
index 236b5c6d99d..311157994e2 100644
--- a/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
+++ b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
@@ -25,24 +25,16 @@ use function explode;
class RateLimitingPlugin extends ServerPlugin {
private Limiter $limiter;
- private IUserManager $userManager;
- private CalDavBackend $calDavBackend;
- private IAppConfig $config;
- private LoggerInterface $logger;
- private ?string $userId;
- public function __construct(Limiter $limiter,
- IUserManager $userManager,
- CalDavBackend $calDavBackend,
- LoggerInterface $logger,
- IAppConfig $config,
- ?string $userId) {
+ public function __construct(
+ Limiter $limiter,
+ private IUserManager $userManager,
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ private IAppConfig $config,
+ private ?string $userId,
+ ) {
$this->limiter = $limiter;
- $this->userManager = $userManager;
- $this->calDavBackend = $calDavBackend;
- $this->config = $config;
- $this->logger = $logger;
- $this->userId = $userId;
}
public function initialize(DAV\Server $server): void {
diff --git a/apps/dav/lib/CalDAV/Sharing/Backend.php b/apps/dav/lib/CalDAV/Sharing/Backend.php
index 40fb7e03e5f..fc5d65b5994 100644
--- a/apps/dav/lib/CalDAV/Sharing/Backend.php
+++ b/apps/dav/lib/CalDAV/Sharing/Backend.php
@@ -17,7 +17,8 @@ use Psr\Log\LoggerInterface;
class Backend extends SharingBackend {
- public function __construct(private IUserManager $userManager,
+ public function __construct(
+ private IUserManager $userManager,
private IGroupManager $groupManager,
private Principal $principalBackend,
private ICacheFactory $cacheFactory,
diff --git a/apps/dav/lib/CalDAV/Sharing/Service.php b/apps/dav/lib/CalDAV/Sharing/Service.php
index 7867b8d1adb..4f0554f09bd 100644
--- a/apps/dav/lib/CalDAV/Sharing/Service.php
+++ b/apps/dav/lib/CalDAV/Sharing/Service.php
@@ -13,7 +13,9 @@ use OCA\DAV\DAV\Sharing\SharingService;
class Service extends SharingService {
protected string $resourceType = 'calendar';
- public function __construct(protected SharingMapper $mapper) {
+ public function __construct(
+ protected SharingMapper $mapper,
+ ) {
parent::__construct($mapper);
}
}
diff --git a/apps/dav/lib/CalDAV/Status/StatusService.php b/apps/dav/lib/CalDAV/Status/StatusService.php
index be8ad90dd45..9ee0e9bf356 100644
--- a/apps/dav/lib/CalDAV/Status/StatusService.php
+++ b/apps/dav/lib/CalDAV/Status/StatusService.php
@@ -27,35 +27,37 @@ use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
class StatusService {
private ICache $cache;
- public function __construct(private ITimeFactory $timeFactory,
+ public function __construct(
+ private ITimeFactory $timeFactory,
private IManager $calendarManager,
private IUserManager $userManager,
private UserStatusService $userStatusService,
private IAvailabilityCoordinator $availabilityCoordinator,
private ICacheFactory $cacheFactory,
- private LoggerInterface $logger) {
+ private LoggerInterface $logger,
+ ) {
$this->cache = $cacheFactory->createLocal('CalendarStatusService');
}
public function processCalendarStatus(string $userId): void {
$user = $this->userManager->get($userId);
- if($user === null) {
+ if ($user === null) {
return;
}
$availability = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
- if($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) {
+ if ($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) {
$this->logger->debug('An Absence is in effect, skipping calendar status check', ['user' => $userId]);
return;
}
$calendarEvents = $this->cache->get($userId);
- if($calendarEvents === null) {
+ if ($calendarEvents === null) {
$calendarEvents = $this->getCalendarEvents($user);
$this->cache->set($userId, $calendarEvents, 300);
}
- if(empty($calendarEvents)) {
+ if (empty($calendarEvents)) {
try {
$this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
} catch (Exception $e) {
@@ -81,9 +83,9 @@ class StatusService {
$currentStatus = null;
}
- if($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL
- || $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND
- || $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE) {
+ if (($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL)
+ || ($currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND)
+ || ($currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE)) {
// We don't overwrite the call status, DND status or Invisible status
$this->logger->debug('Higher priority status detected, skipping calendar status change', ['user' => $userId]);
return;
@@ -101,7 +103,7 @@ class StatusService {
if (isset($component['DTSTART']) && $userStatusTimestamp !== null) {
/** @var DateTimeImmutable $dateTime */
$dateTime = $component['DTSTART'][0];
- if($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) {
+ if ($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) {
return false;
}
}
@@ -112,7 +114,7 @@ class StatusService {
return true;
});
- if(empty($applicableEvents)) {
+ if (empty($applicableEvents)) {
try {
$this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
} catch (Exception $e) {
@@ -130,7 +132,7 @@ class StatusService {
}
// Only update the status if it's neccesary otherwise we mess up the timestamp
- if($currentStatus === null || $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY) {
+ if ($currentStatus === null || $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY) {
// One event that fulfills all status conditions is enough
// 1. Not an OOO event
// 2. Current user status (that is not a calendar status) was not set after the start of this event
@@ -139,7 +141,7 @@ class StatusService {
$this->logger->debug("Found $count applicable event(s), changing user status", ['user' => $userId]);
$this->userStatusService->setUserStatus(
$userId,
- IUserStatus::AWAY,
+ IUserStatus::BUSY,
IUserStatus::MESSAGE_CALENDAR_BUSY,
true
);
@@ -148,7 +150,7 @@ class StatusService {
private function getCalendarEvents(User $user): array {
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $user->getUID());
- if(empty($calendars)) {
+ if (empty($calendars)) {
return [];
}
@@ -172,7 +174,7 @@ class StatusService {
$dtEnd = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+5 minutes'));
// Only query the calendars when there's any to search
- if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) {
+ if ($query instanceof CalendarQuery && !empty($query->getCalendarUris())) {
// Query the next hour
$query->setTimerangeStart($dtStart);
$query->setTimerangeEnd($dtEnd);
diff --git a/apps/dav/lib/CalDAV/TimeZoneFactory.php b/apps/dav/lib/CalDAV/TimeZoneFactory.php
new file mode 100644
index 00000000000..36a2c97be82
--- /dev/null
+++ b/apps/dav/lib/CalDAV/TimeZoneFactory.php
@@ -0,0 +1,213 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use DateTimeZone;
+
+/**
+ * Class to generate DateTimeZone object with automated Microsoft and IANA handling
+ *
+ * @since 31.0.0
+ */
+class TimeZoneFactory {
+
+ /**
+ * conversion table of Microsoft time zones to IANA time zones
+ *
+ * @var array<string,string> MS2IANA
+ */
+ private const MS2IANA = [
+ 'AUS Central Standard Time' => 'Australia/Darwin',
+ 'Aus Central W. Standard Time' => 'Australia/Eucla',
+ 'AUS Eastern Standard Time' => 'Australia/Sydney',
+ 'Afghanistan Standard Time' => 'Asia/Kabul',
+ 'Alaskan Standard Time' => 'America/Anchorage',
+ 'Aleutian Standard Time' => 'America/Adak',
+ 'Altai Standard Time' => 'Asia/Barnaul',
+ 'Arab Standard Time' => 'Asia/Riyadh',
+ 'Arabian Standard Time' => 'Asia/Dubai',
+ 'Arabic Standard Time' => 'Asia/Baghdad',
+ 'Argentina Standard Time' => 'America/Buenos_Aires',
+ 'Astrakhan Standard Time' => 'Europe/Astrakhan',
+ 'Atlantic Standard Time' => 'America/Halifax',
+ 'Azerbaijan Standard Time' => 'Asia/Baku',
+ 'Azores Standard Time' => 'Atlantic/Azores',
+ 'Bahia Standard Time' => 'America/Bahia',
+ 'Bangladesh Standard Time' => 'Asia/Dhaka',
+ 'Belarus Standard Time' => 'Europe/Minsk',
+ 'Bougainville Standard Time' => 'Pacific/Bougainville',
+ 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde',
+ 'Canada Central Standard Time' => 'America/Regina',
+ 'Caucasus Standard Time' => 'Asia/Yerevan',
+ 'Cen. Australia Standard Time' => 'Australia/Adelaide',
+ 'Central America Standard Time' => 'America/Guatemala',
+ 'Central Asia Standard Time' => 'Asia/Almaty',
+ 'Central Brazilian Standard Time' => 'America/Cuiaba',
+ 'Central Europe Standard Time' => 'Europe/Budapest',
+ 'Central European Standard Time' => 'Europe/Warsaw',
+ 'Central Pacific Standard Time' => 'Pacific/Guadalcanal',
+ 'Central Standard Time' => 'America/Chicago',
+ 'Central Standard Time (Mexico)' => 'America/Mexico_City',
+ 'Chatham Islands Standard Time' => 'Pacific/Chatham',
+ 'China Standard Time' => 'Asia/Shanghai',
+ 'Coordinated Universal Time' => 'UTC',
+ 'Cuba Standard Time' => 'America/Havana',
+ 'Dateline Standard Time' => 'Etc/GMT+12',
+ 'E. Africa Standard Time' => 'Africa/Nairobi',
+ 'E. Australia Standard Time' => 'Australia/Brisbane',
+ 'E. Europe Standard Time' => 'Europe/Chisinau',
+ 'E. South America Standard Time' => 'America/Sao_Paulo',
+ 'Easter Island Standard Time' => 'Pacific/Easter',
+ 'Eastern Standard Time' => 'America/Toronto',
+ 'Eastern Standard Time (Mexico)' => 'America/Cancun',
+ 'Egypt Standard Time' => 'Africa/Cairo',
+ 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+ 'FLE Standard Time' => 'Europe/Kiev',
+ 'Fiji Standard Time' => 'Pacific/Fiji',
+ 'GMT Standard Time' => 'Europe/London',
+ 'GTB Standard Time' => 'Europe/Bucharest',
+ 'Georgian Standard Time' => 'Asia/Tbilisi',
+ 'Greenland Standard Time' => 'America/Godthab',
+ 'Greenland (Danmarkshavn)' => 'America/Godthab',
+ 'Greenwich Standard Time' => 'Atlantic/Reykjavik',
+ 'Haiti Standard Time' => 'America/Port-au-Prince',
+ 'Hawaiian Standard Time' => 'Pacific/Honolulu',
+ 'India Standard Time' => 'Asia/Kolkata',
+ 'Iran Standard Time' => 'Asia/Tehran',
+ 'Israel Standard Time' => 'Asia/Jerusalem',
+ 'Jordan Standard Time' => 'Asia/Amman',
+ 'Kaliningrad Standard Time' => 'Europe/Kaliningrad',
+ 'Kamchatka Standard Time' => 'Asia/Kamchatka',
+ 'Korea Standard Time' => 'Asia/Seoul',
+ 'Libya Standard Time' => 'Africa/Tripoli',
+ 'Line Islands Standard Time' => 'Pacific/Kiritimati',
+ 'Lord Howe Standard Time' => 'Australia/Lord_Howe',
+ 'Magadan Standard Time' => 'Asia/Magadan',
+ 'Magallanes Standard Time' => 'America/Punta_Arenas',
+ 'Malaysia Standard Time' => 'Asia/Kuala_Lumpur',
+ 'Marquesas Standard Time' => 'Pacific/Marquesas',
+ 'Mauritius Standard Time' => 'Indian/Mauritius',
+ 'Mid-Atlantic Standard Time' => 'Atlantic/South_Georgia',
+ 'Middle East Standard Time' => 'Asia/Beirut',
+ 'Montevideo Standard Time' => 'America/Montevideo',
+ 'Morocco Standard Time' => 'Africa/Casablanca',
+ 'Mountain Standard Time' => 'America/Denver',
+ 'Mountain Standard Time (Mexico)' => 'America/Chihuahua',
+ 'Myanmar Standard Time' => 'Asia/Rangoon',
+ 'N. Central Asia Standard Time' => 'Asia/Novosibirsk',
+ 'Namibia Standard Time' => 'Africa/Windhoek',
+ 'Nepal Standard Time' => 'Asia/Kathmandu',
+ 'New Zealand Standard Time' => 'Pacific/Auckland',
+ 'Newfoundland Standard Time' => 'America/St_Johns',
+ 'Norfolk Standard Time' => 'Pacific/Norfolk',
+ 'North Asia East Standard Time' => 'Asia/Irkutsk',
+ 'North Asia Standard Time' => 'Asia/Krasnoyarsk',
+ 'North Korea Standard Time' => 'Asia/Pyongyang',
+ 'Omsk Standard Time' => 'Asia/Omsk',
+ 'Pacific SA Standard Time' => 'America/Santiago',
+ 'Pacific Standard Time' => 'America/Los_Angeles',
+ 'Pacific Standard Time (Mexico)' => 'America/Tijuana',
+ 'Pakistan Standard Time' => 'Asia/Karachi',
+ 'Paraguay Standard Time' => 'America/Asuncion',
+ 'Qyzylorda Standard Time' => 'Asia/Qyzylorda',
+ 'Romance Standard Time' => 'Europe/Paris',
+ 'Russian Standard Time' => 'Europe/Moscow',
+ 'Russia Time Zone 10' => 'Asia/Srednekolymsk',
+ 'Russia Time Zone 3' => 'Europe/Samara',
+ 'SA Eastern Standard Time' => 'America/Cayenne',
+ 'SA Pacific Standard Time' => 'America/Bogota',
+ 'SA Western Standard Time' => 'America/La_Paz',
+ 'SE Asia Standard Time' => 'Asia/Bangkok',
+ 'Saint Pierre Standard Time' => 'America/Miquelon',
+ 'Sakhalin Standard Time' => 'Asia/Sakhalin',
+ 'Samoa Standard Time' => 'Pacific/Apia',
+ 'Sao Tome Standard Time' => 'Africa/Sao_Tome',
+ 'Saratov Standard Time' => 'Europe/Saratov',
+ 'Singapore Standard Time' => 'Asia/Singapore',
+ 'South Africa Standard Time' => 'Africa/Johannesburg',
+ 'South Sudan Standard Time' => 'Africa/Juba',
+ 'Sri Lanka Standard Time' => 'Asia/Colombo',
+ 'Sudan Standard Time' => 'Africa/Khartoum',
+ 'Syria Standard Time' => 'Asia/Damascus',
+ 'Taipei Standard Time' => 'Asia/Taipei',
+ 'Tasmania Standard Time' => 'Australia/Hobart',
+ 'Tocantins Standard Time' => 'America/Araguaina',
+ 'Tokyo Standard Time' => 'Asia/Tokyo',
+ 'Tomsk Standard Time' => 'Asia/Tomsk',
+ 'Tonga Standard Time' => 'Pacific/Tongatapu',
+ 'Transbaikal Standard Time' => 'Asia/Chita',
+ 'Turkey Standard Time' => 'Europe/Istanbul',
+ 'Turks And Caicos Standard Time' => 'America/Grand_Turk',
+ 'US Eastern Standard Time' => 'America/Indianapolis',
+ 'US Mountain Standard Time' => 'America/Phoenix',
+ 'UTC' => 'Etc/GMT',
+ 'UTC+13' => 'Etc/GMT-13',
+ 'UTC+12' => 'Etc/GMT-12',
+ 'UTC-02' => 'Etc/GMT+2',
+ 'UTC-09' => 'Etc/GMT+9',
+ 'UTC-11' => 'Etc/GMT+11',
+ 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar',
+ 'Venezuela Standard Time' => 'America/Caracas',
+ 'Vladivostok Standard Time' => 'Asia/Vladivostok',
+ 'Volgograd Standard Time' => 'Europe/Volgograd',
+ 'W. Australia Standard Time' => 'Australia/Perth',
+ 'W. Central Africa Standard Time' => 'Africa/Lagos',
+ 'W. Europe Standard Time' => 'Europe/Berlin',
+ 'W. Mongolia Standard Time' => 'Asia/Hovd',
+ 'West Asia Standard Time' => 'Asia/Tashkent',
+ 'West Bank Standard Time' => 'Asia/Hebron',
+ 'West Pacific Standard Time' => 'Pacific/Port_Moresby',
+ 'West Samoa Standard Time' => 'Pacific/Apia',
+ 'Yakutsk Standard Time' => 'Asia/Yakutsk',
+ 'Yukon Standard Time' => 'America/Whitehorse',
+ 'Yekaterinburg Standard Time' => 'Asia/Yekaterinburg',
+ ];
+
+ /**
+ * Determines if given time zone name is a Microsoft time zone
+ *
+ * @since 31.0.0
+ *
+ * @param string $name time zone name
+ *
+ * @return bool
+ */
+ public static function isMS(string $name): bool {
+ return isset(self::MS2IANA[$name]);
+ }
+
+ /**
+ * Converts Microsoft time zone name to IANA time zone name
+ *
+ * @since 31.0.0
+ *
+ * @param string $name microsoft time zone
+ *
+ * @return string|null valid IANA time zone name on success, or null on failure
+ */
+ public static function toIANA(string $name): ?string {
+ return isset(self::MS2IANA[$name]) ? self::MS2IANA[$name] : null;
+ }
+
+ /**
+ * Generates DateTimeZone object for given time zone name
+ *
+ * @since 31.0.0
+ *
+ * @param string $name time zone name
+ *
+ * @return DateTimeZone|null
+ */
+ public function fromName(string $name): ?DateTimeZone {
+ // if zone name is MS convert to IANA, otherwise just assume the zone is IANA
+ $zone = @timezone_open(self::toIANA($name) ?? $name);
+ return ($zone instanceof DateTimeZone) ? $zone : null;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/TimezoneService.php b/apps/dav/lib/CalDAV/TimezoneService.php
index 93f19be1b55..a7709bde0f9 100644
--- a/apps/dav/lib/CalDAV/TimezoneService.php
+++ b/apps/dav/lib/CalDAV/TimezoneService.php
@@ -20,9 +20,11 @@ use function array_reduce;
class TimezoneService {
- public function __construct(private IConfig $config,
+ public function __construct(
+ private IConfig $config,
private PropertyMapper $propertyMapper,
- private IManager $calendarManager) {
+ private IManager $calendarManager,
+ ) {
}
public function getUserTimezone(string $userId): ?string {
diff --git a/apps/dav/lib/CalDAV/TipBroker.php b/apps/dav/lib/CalDAV/TipBroker.php
new file mode 100644
index 00000000000..16e68fde1f0
--- /dev/null
+++ b/apps/dav/lib/CalDAV/TipBroker.php
@@ -0,0 +1,187 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\ITip\Broker;
+use Sabre\VObject\ITip\Message;
+
+class TipBroker extends Broker {
+
+ public $significantChangeProperties = [
+ 'DTSTART',
+ 'DTEND',
+ 'DURATION',
+ 'DUE',
+ 'RRULE',
+ 'RDATE',
+ 'EXDATE',
+ 'STATUS',
+ 'SUMMARY',
+ 'DESCRIPTION',
+ 'LOCATION',
+
+ ];
+
+ /**
+ * This method is used in cases where an event got updated, and we
+ * potentially need to send emails to attendees to let them know of updates
+ * in the events.
+ *
+ * We will detect which attendees got added, which got removed and create
+ * specific messages for these situations.
+ *
+ * @return array
+ */
+ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) {
+ // Merging attendee lists.
+ $attendees = [];
+ foreach ($oldEventInfo['attendees'] as $attendee) {
+ $attendees[$attendee['href']] = [
+ 'href' => $attendee['href'],
+ 'oldInstances' => $attendee['instances'],
+ 'newInstances' => [],
+ 'name' => $attendee['name'],
+ 'forceSend' => null,
+ ];
+ }
+ foreach ($eventInfo['attendees'] as $attendee) {
+ if (isset($attendees[$attendee['href']])) {
+ $attendees[$attendee['href']]['name'] = $attendee['name'];
+ $attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
+ $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
+ } else {
+ $attendees[$attendee['href']] = [
+ 'href' => $attendee['href'],
+ 'oldInstances' => [],
+ 'newInstances' => $attendee['instances'],
+ 'name' => $attendee['name'],
+ 'forceSend' => $attendee['forceSend'],
+ ];
+ }
+ }
+
+ $messages = [];
+
+ foreach ($attendees as $attendee) {
+ // An organizer can also be an attendee. We should not generate any
+ // messages for those.
+ if ($attendee['href'] === $eventInfo['organizer']) {
+ continue;
+ }
+
+ $message = new Message();
+ $message->uid = $eventInfo['uid'];
+ $message->component = 'VEVENT';
+ $message->sequence = $eventInfo['sequence'];
+ $message->sender = $eventInfo['organizer'];
+ $message->senderName = $eventInfo['organizerName'];
+ $message->recipient = $attendee['href'];
+ $message->recipientName = $attendee['name'];
+
+ // Creating the new iCalendar body.
+ $icalMsg = new VCalendar();
+
+ foreach ($calendar->select('VTIMEZONE') as $timezone) {
+ $icalMsg->add(clone $timezone);
+ }
+ // If there are no instances the attendee is a part of, it means
+ // the attendee was removed and we need to send them a CANCEL message.
+ // Also If the meeting STATUS property was changed to CANCELLED
+ // we need to send the attendee a CANCEL message.
+ if (!$attendee['newInstances'] || $eventInfo['status'] === 'CANCELLED') {
+
+ $message->method = $icalMsg->METHOD = 'CANCEL';
+ $message->significantChange = true;
+ // clone base event
+ $event = clone $eventInfo['instances']['master'];
+ // alter some properties
+ unset($event->ATTENDEE);
+ $event->add('ATTENDEE', $attendee['href'], ['CN' => $attendee['name'],]);
+ $event->DTSTAMP = gmdate('Ymd\\THis\\Z');
+ $event->SEQUENCE = $message->sequence;
+ $icalMsg->add($event);
+
+ } else {
+ // The attendee gets the updated event body
+ $message->method = $icalMsg->METHOD = 'REQUEST';
+
+ // We need to find out that this change is significant. If it's
+ // not, systems may opt to not send messages.
+ //
+ // We do this based on the 'significantChangeHash' which is
+ // some value that changes if there's a certain set of
+ // properties changed in the event, or simply if there's a
+ // difference in instances that the attendee is invited to.
+
+ $oldAttendeeInstances = array_keys($attendee['oldInstances']);
+ $newAttendeeInstances = array_keys($attendee['newInstances']);
+
+ $message->significantChange
+ = $attendee['forceSend'] === 'REQUEST'
+ || count($oldAttendeeInstances) !== count($newAttendeeInstances)
+ || count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0
+ || $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
+
+ foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
+ $currentEvent = clone $eventInfo['instances'][$instanceId];
+ if ($instanceId === 'master') {
+ // We need to find a list of events that the attendee
+ // is not a part of to add to the list of exceptions.
+ $exceptions = [];
+ foreach ($eventInfo['instances'] as $instanceId => $vevent) {
+ if (!isset($attendee['newInstances'][$instanceId])) {
+ $exceptions[] = $instanceId;
+ }
+ }
+
+ // If there were exceptions, we need to add it to an
+ // existing EXDATE property, if it exists.
+ if ($exceptions) {
+ if (isset($currentEvent->EXDATE)) {
+ $currentEvent->EXDATE->setParts(array_merge(
+ $currentEvent->EXDATE->getParts(),
+ $exceptions
+ ));
+ } else {
+ $currentEvent->EXDATE = $exceptions;
+ }
+ }
+
+ // Cleaning up any scheduling information that
+ // shouldn't be sent along.
+ unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
+ unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
+
+ foreach ($currentEvent->ATTENDEE as $attendee) {
+ unset($attendee['SCHEDULE-FORCE-SEND']);
+ unset($attendee['SCHEDULE-STATUS']);
+
+ // We're adding PARTSTAT=NEEDS-ACTION to ensure that
+ // iOS shows an "Inbox Item"
+ if (!isset($attendee['PARTSTAT'])) {
+ $attendee['PARTSTAT'] = 'NEEDS-ACTION';
+ }
+ }
+ }
+
+ $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
+ $icalMsg->add($currentEvent);
+ }
+ }
+
+ $message->message = $icalMsg;
+ $messages[] = $message;
+ }
+
+ return $messages;
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
index 3ef147fe200..d8c429f2056 100644
--- a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
+++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
@@ -18,26 +18,13 @@ use Sabre\DAVACL\IACL;
class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
use ACLTrait;
- /** @var string */
- private $name;
-
- /** @var mixed[] */
- private $objectData;
-
- /** @var string */
- private $principalUri;
-
- /** @var CalDavBackend */
- private $calDavBackend;
-
- public function __construct(string $name,
- array $objectData,
- string $principalUri,
- CalDavBackend $calDavBackend) {
- $this->name = $name;
- $this->objectData = $objectData;
- $this->calDavBackend = $calDavBackend;
- $this->principalUri = $principalUri;
+ public function __construct(
+ private string $name,
+ /** @var mixed[] */
+ private array $objectData,
+ private string $principalUri,
+ private CalDavBackend $calDavBackend,
+ ) {
}
public function delete() {
@@ -72,7 +59,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
public function getContentType() {
$mime = 'text/calendar; charset=utf-8';
if (isset($this->objectData['component']) && $this->objectData['component']) {
- $mime .= '; component='.$this->objectData['component'];
+ $mime .= '; component=' . $this->objectData['component'];
}
return $mime;
@@ -83,7 +70,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
}
public function getSize() {
- return (int) $this->objectData['size'];
+ return (int)$this->objectData['size'];
}
public function restore(): void {
@@ -91,7 +78,7 @@ class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
}
public function getDeletedAt(): ?int {
- return $this->objectData['deleted_at'] ? (int) $this->objectData['deleted_at'] : null;
+ return $this->objectData['deleted_at'] ? (int)$this->objectData['deleted_at'] : null;
}
public function getCalendarUri(): string {
diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
index abf5f001e71..f75e19689f1 100644
--- a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
+++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
@@ -25,16 +25,11 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL
public const NAME = 'objects';
- /** @var CalDavBackend */
- protected $caldavBackend;
-
- /** @var mixed[] */
- private $principalInfo;
-
- public function __construct(CalDavBackend $caldavBackend,
- array $principalInfo) {
- $this->caldavBackend = $caldavBackend;
- $this->principalInfo = $principalInfo;
+ public function __construct(
+ protected CalDavBackend $caldavBackend,
+ /** @var mixed[] */
+ private array $principalInfo,
+ ) {
}
/**
@@ -51,7 +46,7 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL
$data = $this->caldavBackend->getCalendarObjectById(
$this->principalInfo['uri'],
- (int) $matches[1],
+ (int)$matches[1],
);
// If the object hasn't been deleted yet then we don't want to find it here
diff --git a/apps/dav/lib/CalDAV/Trashbin/Plugin.php b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
index f3c8342c475..6f58b1f3110 100644
--- a/apps/dav/lib/CalDAV/Trashbin/Plugin.php
+++ b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
@@ -32,16 +32,14 @@ class Plugin extends ServerPlugin {
/** @var bool */
private $disableTrashbin;
- /** @var RetentionService */
- private $retentionService;
-
/** @var Server */
private $server;
- public function __construct(IRequest $request,
- RetentionService $retentionService) {
+ public function __construct(
+ IRequest $request,
+ private RetentionService $retentionService,
+ ) {
$this->disableTrashbin = $request->getHeader('X-NC-CalDAV-No-Trashbin') === '1';
- $this->retentionService = $retentionService;
}
public function initialize(Server $server): void {
diff --git a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
index a1958bb2794..1c76bd2295d 100644
--- a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
+++ b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
@@ -26,16 +26,10 @@ class TrashbinHome implements IACL, ICollection, IProperties {
public const NAME = 'trashbin';
- /** @var CalDavBackend */
- private $caldavBackend;
-
- /** @var array */
- private $principalInfo;
-
- public function __construct(CalDavBackend $caldavBackend,
- array $principalInfo) {
- $this->caldavBackend = $caldavBackend;
- $this->principalInfo = $principalInfo;
+ public function __construct(
+ private CalDavBackend $caldavBackend,
+ private array $principalInfo,
+ ) {
}
public function getOwner(): string {
diff --git a/apps/dav/lib/CalDAV/UpcomingEvent.php b/apps/dav/lib/CalDAV/UpcomingEvent.php
new file mode 100644
index 00000000000..e8b604f460a
--- /dev/null
+++ b/apps/dav/lib/CalDAV/UpcomingEvent.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use JsonSerializable;
+use OCA\DAV\ResponseDefinitions;
+
+class UpcomingEvent implements JsonSerializable {
+ public function __construct(
+ private string $uri,
+ private ?int $recurrenceId,
+ private string $calendarUri,
+ private ?int $start,
+ private ?string $summary,
+ private ?string $location,
+ private ?string $calendarAppUrl,
+ ) {
+ }
+
+ public function getUri(): string {
+ return $this->uri;
+ }
+
+ public function getRecurrenceId(): ?int {
+ return $this->recurrenceId;
+ }
+
+ public function getCalendarUri(): string {
+ return $this->calendarUri;
+ }
+
+ public function getStart(): ?int {
+ return $this->start;
+ }
+
+ public function getSummary(): ?string {
+ return $this->summary;
+ }
+
+ public function getLocation(): ?string {
+ return $this->location;
+ }
+
+ public function getCalendarAppUrl(): ?string {
+ return $this->calendarAppUrl;
+ }
+
+ /**
+ * @see ResponseDefinitions
+ */
+ public function jsonSerialize(): array {
+ return [
+ 'uri' => $this->uri,
+ 'recurrenceId' => $this->recurrenceId,
+ 'calendarUri' => $this->calendarUri,
+ 'start' => $this->start,
+ 'summary' => $this->summary,
+ 'location' => $this->location,
+ 'calendarAppUrl' => $this->calendarAppUrl,
+ ];
+ }
+}
diff --git a/apps/dav/lib/CalDAV/UpcomingEventsService.php b/apps/dav/lib/CalDAV/UpcomingEventsService.php
new file mode 100644
index 00000000000..1a8aed5bd71
--- /dev/null
+++ b/apps/dav/lib/CalDAV/UpcomingEventsService.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use OCP\App\IAppManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\IManager;
+use OCP\IURLGenerator;
+use OCP\IUserManager;
+use function array_map;
+
+class UpcomingEventsService {
+ public function __construct(
+ private IManager $calendarManager,
+ private ITimeFactory $timeFactory,
+ private IUserManager $userManager,
+ private IAppManager $appManager,
+ private IURLGenerator $urlGenerator,
+ ) {
+ }
+
+ /**
+ * @return UpcomingEvent[]
+ */
+ public function getEvents(string $userId, ?string $location = null): array {
+ $searchQuery = $this->calendarManager->newQuery('principals/users/' . $userId);
+ if ($location !== null) {
+ $searchQuery->addSearchProperty('LOCATION');
+ $searchQuery->setSearchPattern($location);
+ }
+ $searchQuery->addType('VEVENT');
+ $searchQuery->setLimit(3);
+ $now = $this->timeFactory->now();
+ $searchQuery->setTimerangeStart($now->modify('-1 minute'));
+ $searchQuery->setTimerangeEnd($now->modify('+1 month'));
+
+ $events = $this->calendarManager->searchForPrincipal($searchQuery);
+ $calendarAppEnabled = $this->appManager->isEnabledForUser(
+ 'calendar',
+ $this->userManager->get($userId),
+ );
+
+ return array_filter(array_map(function (array $event) use ($userId, $calendarAppEnabled) {
+ $calendarAppUrl = null;
+
+ if ($calendarAppEnabled) {
+ $arguments = [
+ 'objectId' => base64_encode($this->urlGenerator->getWebroot() . '/remote.php/dav/calendars/' . $userId . '/' . $event['calendar-uri'] . '/' . $event['uri']),
+ ];
+
+ if (isset($event['RECURRENCE-ID'])) {
+ $arguments['recurrenceId'] = $event['RECURRENCE-ID'][0];
+ }
+ /**
+ * TODO: create a named, deep route in calendar (it's a code smell to just assume this route exists, find an abstraction)
+ * When changing, also adjust for:
+ * - spreed/lib/Service/CalendarIntegrationService.php#getDashboardEvents
+ * - spreed/lib/Service/CalendarIntegrationService.php#getMutualEvents
+ */
+ $calendarAppUrl = $this->urlGenerator->linkToRouteAbsolute('calendar.view.indexdirect.edit', $arguments);
+ }
+
+ if (isset($event['objects'][0]['STATUS']) && $event['objects'][0]['STATUS'][0] === 'CANCELLED') {
+ return false;
+ }
+
+ return new UpcomingEvent(
+ $event['uri'],
+ ($event['objects'][0]['RECURRENCE-ID'][0] ?? null)?->getTimeStamp(),
+ $event['calendar-uri'],
+ $event['objects'][0]['DTSTART'][0]?->getTimestamp(),
+ $event['objects'][0]['SUMMARY'][0] ?? null,
+ $event['objects'][0]['LOCATION'][0] ?? null,
+ $calendarAppUrl,
+ );
+ }, $events));
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php b/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php
new file mode 100644
index 00000000000..b647e63e67b
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Validation/CalDavValidatePlugin.php
@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV\Validation;
+
+use OCA\DAV\AppInfo\Application;
+use OCP\IAppConfig;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+class CalDavValidatePlugin extends ServerPlugin {
+
+ public function __construct(
+ private IAppConfig $config,
+ ) {
+ }
+
+ public function initialize(Server $server): void {
+ $server->on('beforeMethod:PUT', [$this, 'beforePut']);
+ }
+
+ public function beforePut(RequestInterface $request, ResponseInterface $response): bool {
+ // evaluate if card size exceeds defined limit
+ $eventSizeLimit = $this->config->getValueInt(Application::APP_ID, 'event_size_limit', 10485760);
+ if ((int)$request->getRawServerValue('CONTENT_LENGTH') > $eventSizeLimit) {
+ throw new Forbidden("VEvent or VTodo object exceeds $eventSizeLimit bytes");
+ }
+ // all tests passed return true
+ return true;
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/WebcalCaching/Connection.php b/apps/dav/lib/CalDAV/WebcalCaching/Connection.php
new file mode 100644
index 00000000000..3d12c92c49a
--- /dev/null
+++ b/apps/dav/lib/CalDAV/WebcalCaching/Connection.php
@@ -0,0 +1,143 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV\WebcalCaching;
+
+use Exception;
+use GuzzleHttp\RequestOptions;
+use OCP\Http\Client\IClientService;
+use OCP\Http\Client\LocalServerException;
+use OCP\IAppConfig;
+use Psr\Log\LoggerInterface;
+use Sabre\VObject\Reader;
+
+class Connection {
+ public function __construct(
+ private IClientService $clientService,
+ private IAppConfig $config,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * gets webcal feed from remote server
+ */
+ public function queryWebcalFeed(array $subscription): ?string {
+ $subscriptionId = $subscription['id'];
+ $url = $this->cleanURL($subscription['source']);
+ if ($url === null) {
+ return null;
+ }
+
+ $allowLocalAccess = $this->config->getValueString('dav', 'webcalAllowLocalAccess', 'no');
+
+ $params = [
+ 'nextcloud' => [
+ 'allow_local_address' => $allowLocalAccess === 'yes',
+ ],
+ RequestOptions::HEADERS => [
+ 'User-Agent' => 'Nextcloud Webcal Service',
+ 'Accept' => 'text/calendar, application/calendar+json, application/calendar+xml',
+ ],
+ ];
+
+ $user = parse_url($subscription['source'], PHP_URL_USER);
+ $pass = parse_url($subscription['source'], PHP_URL_PASS);
+ if ($user !== null && $pass !== null) {
+ $params[RequestOptions::AUTH] = [$user, $pass];
+ }
+
+ try {
+ $client = $this->clientService->newClient();
+ $response = $client->get($url, $params);
+ } catch (LocalServerException $ex) {
+ $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules", [
+ 'exception' => $ex,
+ ]);
+ return null;
+ } catch (Exception $ex) {
+ $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error", [
+ 'exception' => $ex,
+ ]);
+ return null;
+ }
+
+ $body = $response->getBody();
+
+ $contentType = $response->getHeader('Content-Type');
+ $contentType = explode(';', $contentType, 2)[0];
+ switch ($contentType) {
+ case 'application/calendar+json':
+ try {
+ $jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
+ } catch (Exception $ex) {
+ // In case of a parsing error return null
+ $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
+ return null;
+ }
+ return $jCalendar->serialize();
+
+ case 'application/calendar+xml':
+ try {
+ $xCalendar = Reader::readXML($body);
+ } catch (Exception $ex) {
+ // In case of a parsing error return null
+ $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
+ return null;
+ }
+ return $xCalendar->serialize();
+
+ case 'text/calendar':
+ default:
+ try {
+ $vCalendar = Reader::read($body);
+ } catch (Exception $ex) {
+ // In case of a parsing error return null
+ $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
+ return null;
+ }
+ return $vCalendar->serialize();
+ }
+ }
+
+ /**
+ * This method will strip authentication information and replace the
+ * 'webcal' or 'webcals' protocol scheme
+ *
+ * @param string $url
+ * @return string|null
+ */
+ private function cleanURL(string $url): ?string {
+ $parsed = parse_url($url);
+ if ($parsed === false) {
+ return null;
+ }
+
+ if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
+ $scheme = 'http';
+ } else {
+ $scheme = 'https';
+ }
+
+ $host = $parsed['host'] ?? '';
+ $port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
+ $path = $parsed['path'] ?? '';
+ $query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
+ $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
+
+ $cleanURL = "$scheme://$host$port$path$query$fragment";
+ // parse_url is giving some weird results if no url and no :// is given,
+ // so let's test the url again
+ $parsedClean = parse_url($cleanURL);
+ if ($parsedClean === false || !isset($parsedClean['host'])) {
+ return null;
+ }
+
+ return $cleanURL;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php b/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php
index e93c536c846..e07be39c7b4 100644
--- a/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php
+++ b/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php
@@ -23,10 +23,14 @@ class Plugin extends ServerPlugin {
* that do not support subscriptions on their own
*
* /^MSFT-WIN-3/ - Windows 10 Calendar
+ * /Evolution/ - Gnome Calendar/Evolution
+ * /KIO/ - KDE PIM/Akonadi
* @var string[]
*/
public const ENABLE_FOR_CLIENTS = [
- "/^MSFT-WIN-3/"
+ '/^MSFT-WIN-3/',
+ '/Evolution/',
+ '/KIO/'
];
/**
diff --git a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
index 209365ea5e4..a0981e6dec1 100644
--- a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
+++ b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
@@ -8,19 +8,12 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CalDAV\WebcalCaching;
-use Exception;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\Middleware;
use OCA\DAV\CalDAV\CalDavBackend;
-use OCP\Http\Client\IClientService;
-use OCP\Http\Client\LocalServerException;
-use OCP\IConfig;
-use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ResponseInterface;
+use OCP\AppFramework\Utility\ITimeFactory;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\PropPatch;
-use Sabre\DAV\Xml\Property\Href;
use Sabre\VObject\Component;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
@@ -33,25 +26,17 @@ use function count;
class RefreshWebcalService {
- private CalDavBackend $calDavBackend;
-
- private IClientService $clientService;
-
- private IConfig $config;
-
- /** @var LoggerInterface */
- private LoggerInterface $logger;
-
public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate';
public const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms';
public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments';
public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos';
- public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, LoggerInterface $logger) {
- $this->calDavBackend = $calDavBackend;
- $this->clientService = $clientService;
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ private Connection $connection,
+ private ITimeFactory $time,
+ ) {
}
public function refreshSubscription(string $principalUri, string $uri) {
@@ -61,11 +46,25 @@ class RefreshWebcalService {
return;
}
- $webcalData = $this->queryWebcalFeed($subscription, $mutations);
+ // Check the refresh rate if there is any
+ if (!empty($subscription['{http://apple.com/ns/ical/}refreshrate'])) {
+ // add the refresh interval to the lastmodified timestamp
+ $refreshInterval = new \DateInterval($subscription['{http://apple.com/ns/ical/}refreshrate']);
+ $updateTime = $this->time->getDateTime();
+ $updateTime->setTimestamp($subscription['lastmodified'])->add($refreshInterval);
+ if ($updateTime->getTimestamp() > $this->time->getTime()) {
+ return;
+ }
+ }
+
+
+ $webcalData = $this->connection->queryWebcalFeed($subscription);
if (!$webcalData) {
return;
}
+ $localData = $this->calDavBackend->getLimitedCalendarObjects((int)$subscription['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+
$stripTodos = ($subscription[self::STRIP_TODOS] ?? 1) === 1;
$stripAlarms = ($subscription[self::STRIP_ALARMS] ?? 1) === 1;
$stripAttachments = ($subscription[self::STRIP_ATTACHMENTS] ?? 1) === 1;
@@ -73,14 +72,10 @@ class RefreshWebcalService {
try {
$splitter = new ICalendar($webcalData, Reader::OPTION_FORGIVING);
- // we wait with deleting all outdated events till we parsed the new ones
- // in case the new calendar is broken and `new ICalendar` throws a ParseException
- // the user will still see the old data
- $this->calDavBackend->purgeAllCachedEventsForSubscription($subscription['id']);
-
while ($vObject = $splitter->getNext()) {
/** @var Component $vObject */
$compName = null;
+ $uid = null;
foreach ($vObject->getComponents() as $component) {
if ($component->name === 'VTIMEZONE') {
@@ -95,21 +90,68 @@ class RefreshWebcalService {
if ($stripAttachments) {
unset($component->{'ATTACH'});
}
+
+ $uid = $component->{ 'UID' }->getValue();
}
if ($stripTodos && $compName === 'VTODO') {
continue;
}
- $objectUri = $this->getRandomCalendarObjectUri();
- $calendarData = $vObject->serialize();
+ if (!isset($uid)) {
+ continue;
+ }
+
try {
- $this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
- } catch (NoInstancesException | BadRequest $ex) {
- $this->logger->error('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
+ $denormalized = $this->calDavBackend->getDenormalizedData($vObject->serialize());
+ } catch (InvalidDataException|Forbidden $ex) {
+ $this->logger->warning('Unable to denormalize calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
+ continue;
+ }
+
+ // Find all identical sets and remove them from the update
+ if (isset($localData[$uid]) && $denormalized['etag'] === $localData[$uid]['etag']) {
+ unset($localData[$uid]);
+ continue;
+ }
+
+ $vObjectCopy = clone $vObject;
+ $identical = isset($localData[$uid]) && $this->compareWithoutDtstamp($vObjectCopy, $localData[$uid]);
+ if ($identical) {
+ unset($localData[$uid]);
+ continue;
+ }
+
+ // Find all modified sets and update them
+ if (isset($localData[$uid]) && $denormalized['etag'] !== $localData[$uid]['etag']) {
+ $this->calDavBackend->updateCalendarObject($subscription['id'], $localData[$uid]['uri'], $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+ unset($localData[$uid]);
+ continue;
+ }
+
+ // Only entirely new events get created here
+ try {
+ $objectUri = $this->getRandomCalendarObjectUri();
+ $this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $vObject->serialize(), CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
+ } catch (NoInstancesException|BadRequest $ex) {
+ $this->logger->warning('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]);
}
}
+ $ids = array_map(static function ($dataSet): int {
+ return (int)$dataSet['id'];
+ }, $localData);
+ $uris = array_map(static function ($dataSet): string {
+ return $dataSet['uri'];
+ }, $localData);
+
+ if (!empty($ids) && !empty($uris)) {
+ // Clean up on aisle 5
+ // The only events left over in the $localData array should be those that don't exist upstream
+ // All deleted VObjects from upstream are removed
+ $this->calDavBackend->purgeCachedEventsForSubscription($subscription['id'], $ids, $uris);
+ }
+
$newRefreshRate = $this->checkWebcalDataForRefreshRate($subscription, $webcalData);
if ($newRefreshRate) {
$mutations[self::REFRESH_RATE] = $newRefreshRate;
@@ -117,7 +159,7 @@ class RefreshWebcalService {
$this->updateSubscription($subscription, $mutations);
} catch (ParseException $ex) {
- $this->logger->error("Subscription {subscriptionId} could not be refreshed due to a parsing error", ['exception' => $ex, 'subscriptionId' => $subscription['id']]);
+ $this->logger->error('Subscription {subscriptionId} could not be refreshed due to a parsing error', ['exception' => $ex, 'subscriptionId' => $subscription['id']]);
}
}
@@ -139,111 +181,6 @@ class RefreshWebcalService {
return $subscriptions[0];
}
- /**
- * gets webcal feed from remote server
- */
- private function queryWebcalFeed(array $subscription, array &$mutations): ?string {
- $client = $this->clientService->newClient();
-
- $didBreak301Chain = false;
- $latestLocation = null;
-
- $handlerStack = HandlerStack::create();
- $handlerStack->push(Middleware::mapRequest(function (RequestInterface $request) {
- return $request
- ->withHeader('Accept', 'text/calendar, application/calendar+json, application/calendar+xml')
- ->withHeader('User-Agent', 'Nextcloud Webcal Service');
- }));
- $handlerStack->push(Middleware::mapResponse(function (ResponseInterface $response) use (&$didBreak301Chain, &$latestLocation) {
- if (!$didBreak301Chain) {
- if ($response->getStatusCode() !== 301) {
- $didBreak301Chain = true;
- } else {
- $latestLocation = $response->getHeader('Location');
- }
- }
- return $response;
- }));
-
- $allowLocalAccess = $this->config->getAppValue('dav', 'webcalAllowLocalAccess', 'no');
- $subscriptionId = $subscription['id'];
- $url = $this->cleanURL($subscription['source']);
- if ($url === null) {
- return null;
- }
-
- try {
- $params = [
- 'allow_redirects' => [
- 'redirects' => 10
- ],
- 'handler' => $handlerStack,
- 'nextcloud' => [
- 'allow_local_address' => $allowLocalAccess === 'yes',
- ]
- ];
-
- $user = parse_url($subscription['source'], PHP_URL_USER);
- $pass = parse_url($subscription['source'], PHP_URL_PASS);
- if ($user !== null && $pass !== null) {
- $params['auth'] = [$user, $pass];
- }
-
- $response = $client->get($url, $params);
- $body = $response->getBody();
-
- if ($latestLocation) {
- $mutations['{http://calendarserver.org/ns/}source'] = new Href($latestLocation);
- }
-
- $contentType = $response->getHeader('Content-Type');
- $contentType = explode(';', $contentType, 2)[0];
- switch ($contentType) {
- case 'application/calendar+json':
- try {
- $jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING);
- } catch (Exception $ex) {
- // In case of a parsing error return null
- $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
- return null;
- }
- return $jCalendar->serialize();
-
- case 'application/calendar+xml':
- try {
- $xCalendar = Reader::readXML($body);
- } catch (Exception $ex) {
- // In case of a parsing error return null
- $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
- return null;
- }
- return $xCalendar->serialize();
-
- case 'text/calendar':
- default:
- try {
- $vCalendar = Reader::read($body);
- } catch (Exception $ex) {
- // In case of a parsing error return null
- $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]);
- return null;
- }
- return $vCalendar->serialize();
- }
- } catch (LocalServerException $ex) {
- $this->logger->warning("Subscription $subscriptionId was not refreshed because it violates local access rules", [
- 'exception' => $ex,
- ]);
-
- return null;
- } catch (Exception $ex) {
- $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a network error", [
- 'exception' => $ex,
- ]);
-
- return null;
- }
- }
/**
* check if:
@@ -304,47 +241,24 @@ class RefreshWebcalService {
}
/**
- * This method will strip authentication information and replace the
- * 'webcal' or 'webcals' protocol scheme
+ * Returns a random uri for a calendar-object
*
- * @param string $url
- * @return string|null
+ * @return string
*/
- private function cleanURL(string $url): ?string {
- $parsed = parse_url($url);
- if ($parsed === false) {
- return null;
- }
+ public function getRandomCalendarObjectUri():string {
+ return UUIDUtil::getUUID() . '.ics';
+ }
- if (isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
- $scheme = 'http';
- } else {
- $scheme = 'https';
+ private function compareWithoutDtstamp(Component $vObject, array $calendarObject): bool {
+ foreach ($vObject->getComponents() as $component) {
+ unset($component->{'DTSTAMP'});
}
- $host = $parsed['host'] ?? '';
- $port = isset($parsed['port']) ? ':' . $parsed['port'] : '';
- $path = $parsed['path'] ?? '';
- $query = isset($parsed['query']) ? '?' . $parsed['query'] : '';
- $fragment = isset($parsed['fragment']) ? '#' . $parsed['fragment'] : '';
-
- $cleanURL = "$scheme://$host$port$path$query$fragment";
- // parse_url is giving some weird results if no url and no :// is given,
- // so let's test the url again
- $parsedClean = parse_url($cleanURL);
- if ($parsedClean === false || !isset($parsedClean['host'])) {
- return null;
+ $localVobject = Reader::read($calendarObject['calendardata']);
+ foreach ($localVobject->getComponents() as $component) {
+ unset($component->{'DTSTAMP'});
}
- return $cleanURL;
- }
-
- /**
- * Returns a random uri for a calendar-object
- *
- * @return string
- */
- public function getRandomCalendarObjectUri():string {
- return UUIDUtil::getUUID() . '.ics';
+ return strcasecmp($localVobject->serialize(), $vObject->serialize()) === 0;
}
}