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.php189
-rw-r--r--apps/dav/lib/CalDAV/Activity/Filter/Calendar.php40
-rw-r--r--apps/dav/lib/CalDAV/Activity/Filter/Todo.php41
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Base.php98
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Calendar.php56
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Event.php137
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Todo.php83
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php30
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Calendar.php27
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Event.php27
-rw-r--r--apps/dav/lib/CalDAV/Activity/Setting/Todo.php29
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php194
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php58
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php134
-rw-r--r--apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php21
-rw-r--r--apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php21
-rw-r--r--apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php62
-rw-r--r--apps/dav/lib/CalDAV/BirthdayService.php245
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscription.php49
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionImpl.php102
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionObject.php23
-rw-r--r--apps/dav/lib/CalDAV/CachedSubscriptionProvider.php40
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php2841
-rw-r--r--apps/dav/lib/CalDAV/Calendar.php153
-rw-r--r--apps/dav/lib/CalDAV/CalendarHome.php76
-rw-r--r--apps/dav/lib/CalDAV/CalendarImpl.php264
-rw-r--r--apps/dav/lib/CalDAV/CalendarManager.php47
-rw-r--r--apps/dav/lib/CalDAV/CalendarObject.php75
-rw-r--r--apps/dav/lib/CalDAV/CalendarProvider.php106
-rw-r--r--apps/dav/lib/CalDAV/CalendarRoot.php46
-rw-r--r--apps/dav/lib/CalDAV/DefaultCalendarValidator.php41
-rw-r--r--apps/dav/lib/CalDAV/EmbeddedCalDavServer.php122
-rw-r--r--apps/dav/lib/CalDAV/EventComparisonService.php100
-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/FreeBusy/FreeBusyGenerator.php26
-rw-r--r--apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php40
-rw-r--r--apps/dav/lib/CalDAV/IRestorable.php21
-rw-r--r--apps/dav/lib/CalDAV/Integration/ExternalCalendar.php39
-rw-r--r--apps/dav/lib/CalDAV/Integration/ICalendarProvider.php22
-rw-r--r--apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php71
-rw-r--r--apps/dav/lib/CalDAV/Outbox.php32
-rw-r--r--apps/dav/lib/CalDAV/Plugin.php24
-rw-r--r--apps/dav/lib/CalDAV/Principal/Collection.php23
-rw-r--r--apps/dav/lib/CalDAV/Principal/User.php23
-rw-r--r--apps/dav/lib/CalDAV/Proxy/Proxy.php28
-rw-r--r--apps/dav/lib/CalDAV/Proxy/ProxyMapper.php25
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendar.php26
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendarObject.php23
-rw-r--r--apps/dav/lib/CalDAV/PublicCalendarRoot.php50
-rw-r--r--apps/dav/lib/CalDAV/Publishing/PublishPlugin.php169
-rw-r--r--apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php42
-rw-r--r--apps/dav/lib/CalDAV/Reminder/Backend.php99
-rw-r--r--apps/dav/lib/CalDAV/Reminder/INotificationProvider.php31
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php73
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php21
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php197
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php21
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php80
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php33
-rw-r--r--apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php21
-rw-r--r--apps/dav/lib/CalDAV/Reminder/Notifier.php96
-rw-r--r--apps/dav/lib/CalDAV/Reminder/ReminderService.php341
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php110
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php38
-rw-r--r--apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php38
-rw-r--r--apps/dav/lib/CalDAV/RetentionService.php45
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php791
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipService.php1294
-rw-r--r--apps/dav/lib/CalDAV/Schedule/Plugin.php301
-rw-r--r--apps/dav/lib/CalDAV/Search/SearchPlugin.php35
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php24
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php25
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php25
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php24
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php24
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php24
-rw-r--r--apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php24
-rw-r--r--apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php87
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Backend.php30
-rw-r--r--apps/dav/lib/CalDAV/Sharing/Service.php21
-rw-r--r--apps/dav/lib/CalDAV/Status/StatusService.php186
-rw-r--r--apps/dav/lib/CalDAV/TimeZoneFactory.php213
-rw-r--r--apps/dav/lib/CalDAV/TimezoneService.php93
-rw-r--r--apps/dav/lib/CalDAV/TipBroker.php187
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php54
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php63
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/Plugin.php31
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php21
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php35
-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.php52
-rw-r--r--apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php329
98 files changed, 8058 insertions, 4778 deletions
diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php
index 84ba50b8c37..f0c49e6e28c 100644
--- a/apps/dav/lib/CalDAV/Activity/Backend.php
+++ b/apps/dav/lib/CalDAV/Activity/Backend.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity;
@@ -34,6 +15,7 @@ use OCP\App\IAppManager;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
+use OCP\IUserManager;
use OCP\IUserSession;
use Sabre\VObject\Reader;
@@ -44,23 +26,13 @@ use Sabre\VObject\Reader;
*/
class Backend {
- /** @var IActivityManager */
- protected $activityManager;
-
- /** @var IGroupManager */
- protected $groupManager;
-
- /** @var IUserSession */
- protected $userSession;
-
- /** @var IAppManager */
- protected $appManager;
-
- public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, IAppManager $appManager) {
- $this->activityManager = $activityManager;
- $this->groupManager = $groupManager;
- $this->userSession = $userSession;
- $this->appManager = $appManager;
+ public function __construct(
+ protected IActivityManager $activityManager,
+ protected IGroupManager $groupManager,
+ protected IUserSession $userSession,
+ protected IAppManager $appManager,
+ protected IUserManager $userManager,
+ ) {
}
/**
@@ -119,7 +91,7 @@ class Backend {
* @param array $calendarData
* @param bool $publishStatus
*/
- public function onCalendarPublication(array $calendarData, $publishStatus) {
+ public function onCalendarPublication(array $calendarData, bool $publishStatus): void {
$this->triggerCalendarActivity($publishStatus ? Calendar::SUBJECT_PUBLISH : Calendar::SUBJECT_UNPUBLISH, $calendarData);
}
@@ -148,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);
@@ -165,13 +137,18 @@ class Backend {
}
foreach ($users as $user) {
+ if ($action === Calendar::SUBJECT_DELETE && !$this->userManager->userExists($user)) {
+ // Avoid creating calendar_delete activities for deleted users
+ continue;
+ }
+
$event->setAffectedUser($user)
->setSubject(
$user === $currentUser ? $action . '_self' : $action,
[
'actor' => $currentUser,
'calendar' => [
- 'id' => (int) $calendarData['id'],
+ 'id' => (int)$calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
@@ -202,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);
@@ -227,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'],
],
@@ -256,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'],
],
@@ -298,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'],
],
@@ -325,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'],
],
@@ -403,7 +380,7 @@ class Backend {
[
'actor' => $event->getAuthor(),
'calendar' => [
- 'id' => (int) $properties['id'],
+ 'id' => (int)$properties['id'],
'uri' => $properties['uri'],
'name' => $properties['{DAV:}displayname'],
],
@@ -438,17 +415,22 @@ class Backend {
$classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC;
$object = $this->getObjectNameAndType($objectData);
+
+ if (!$object) {
+ return;
+ }
+
$action = $action . '_' . $object['type'];
- if ($object['type'] === 'todo' && strpos($action, Event::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'COMPLETED') {
+ if ($object['type'] === 'todo' && str_starts_with($action, Event::SUBJECT_OBJECT_UPDATE) && $object['status'] === 'COMPLETED') {
$action .= '_completed';
- } elseif ($object['type'] === 'todo' && strpos($action, Event::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'NEEDS-ACTION') {
+ } elseif ($object['type'] === 'todo' && str_starts_with($action, Event::SUBJECT_OBJECT_UPDATE) && $object['status'] === 'NEEDS-ACTION') {
$action .= '_needs_action';
}
$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);
@@ -465,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'],
],
@@ -476,7 +458,7 @@ class Backend {
],
];
- if ($object['type'] === 'event' && strpos($action, Event::SUBJECT_OBJECT_DELETE) === false && $this->appManager->isEnabledForUser('calendar')) {
+ if ($object['type'] === 'event' && !str_contains($action, Event::SUBJECT_OBJECT_DELETE) && $this->appManager->isEnabledForUser('calendar')) {
$params['object']['link']['object_uri'] = $objectData['uri'];
$params['object']['link']['calendar_uri'] = $calendarData['uri'];
$params['object']['link']['owner'] = $owner;
@@ -494,8 +476,103 @@ class Backend {
}
/**
+ * Creates activities when a calendar object was moved
+ */
+ public function onMovedCalendarObject(array $sourceCalendarData, array $targetCalendarData, array $sourceShares, array $targetShares, array $objectData): void {
+ if (!isset($targetCalendarData['principaluri'])) {
+ return;
+ }
+
+ $sourcePrincipal = explode('/', $sourceCalendarData['principaluri']);
+ $sourceOwner = array_pop($sourcePrincipal);
+
+ $targetPrincipal = explode('/', $targetCalendarData['principaluri']);
+ $targetOwner = array_pop($targetPrincipal);
+
+ if ($sourceOwner !== $targetOwner) {
+ $this->onTouchCalendarObject(
+ Event::SUBJECT_OBJECT_DELETE,
+ $sourceCalendarData,
+ $sourceShares,
+ $objectData
+ );
+ $this->onTouchCalendarObject(
+ Event::SUBJECT_OBJECT_ADD,
+ $targetCalendarData,
+ $targetShares,
+ $objectData
+ );
+ return;
+ }
+
+ $currentUser = $this->userSession->getUser();
+ if ($currentUser instanceof IUser) {
+ $currentUser = $currentUser->getUID();
+ } else {
+ $currentUser = $targetOwner;
+ }
+
+ $classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC;
+ $object = $this->getObjectNameAndType($objectData);
+
+ if (!$object) {
+ return;
+ }
+
+ $event = $this->activityManager->generateEvent();
+ $event->setApp('dav')
+ ->setObject('calendar', (int)$targetCalendarData['id'])
+ ->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo')
+ ->setAuthor($currentUser);
+
+ $users = $this->getUsersForShares(array_intersect($sourceShares, $targetShares));
+ $users[] = $targetOwner;
+
+ // Users for share can return the owner itself if the calendar is published
+ foreach (array_unique($users) as $user) {
+ if ($classification === CalDavBackend::CLASSIFICATION_PRIVATE && $user !== $targetOwner) {
+ // Private events are only shown to the owner
+ continue;
+ }
+
+ $params = [
+ 'actor' => $event->getAuthor(),
+ 'sourceCalendar' => [
+ 'id' => (int)$sourceCalendarData['id'],
+ 'uri' => $sourceCalendarData['uri'],
+ 'name' => $sourceCalendarData['{DAV:}displayname'],
+ ],
+ 'targetCalendar' => [
+ 'id' => (int)$targetCalendarData['id'],
+ 'uri' => $targetCalendarData['uri'],
+ 'name' => $targetCalendarData['{DAV:}displayname'],
+ ],
+ 'object' => [
+ 'id' => $object['id'],
+ 'name' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $targetOwner ? 'Busy' : $object['name'],
+ 'classified' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $targetOwner,
+ ],
+ ];
+
+ if ($object['type'] === 'event' && $this->appManager->isEnabledForUser('calendar')) {
+ $params['object']['link']['object_uri'] = $objectData['uri'];
+ $params['object']['link']['calendar_uri'] = $targetCalendarData['uri'];
+ $params['object']['link']['owner'] = $targetOwner;
+ }
+
+ $event->setAffectedUser($user)
+ ->setSubject(
+ $user === $currentUser ? Event::SUBJECT_OBJECT_MOVE . '_' . $object['type'] . '_self' : Event::SUBJECT_OBJECT_MOVE . '_' . $object['type'],
+ $params
+ );
+
+ $this->activityManager->publish($event);
+ }
+ }
+
+ /**
* @param array $objectData
- * @return string[]|bool
+ * @return string[]|false
*/
protected function getObjectNameAndType(array $objectData) {
$vObject = Reader::read($objectData['calendardata']);
@@ -513,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 06258e3cf74..78579ee84b7 100644
--- a/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Filter/Calendar.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Filter;
@@ -29,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,
+ ) {
}
/**
@@ -58,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 f727c10befe..b001f90c28d 100644
--- a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Filter;
@@ -28,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,
+ ) {
}
/**
@@ -52,13 +31,13 @@ class Todo implements IFilter {
* @since 11.0.0
*/
public function getName() {
- return $this->l->t('Todos');
+ return $this->l->t('Tasks');
}
/**
* @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 7f70980a72b..558abe0ca1a 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Base.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
@@ -30,51 +13,26 @@ use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
-use OCP\IUser;
use OCP\IUserManager;
abstract class Base implements IProvider {
-
- /** @var IUserManager */
- protected $userManager;
-
- /** @var string[] */
- protected $userDisplayNames = [];
-
- /** @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,
+ ) {
}
- /**
- * @param IEvent $event
- * @param string $subject
- * @param array $parameters
- */
- protected function setSubjects(IEvent $event, $subject, array $parameters) {
- $placeholders = $replacements = [];
- foreach ($parameters as $placeholder => $parameter) {
- $placeholders[] = '{' . $placeholder . '}';
- $replacements[] = $parameter['name'];
- }
-
- $event->setParsedSubject(str_replace($placeholders, $replacements, $subject))
- ->setRichSubject($subject, $parameters);
+ protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
+ $event->setRichSubject($subject, $parameters);
}
/**
@@ -83,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'],
];
}
@@ -107,40 +65,20 @@ abstract class Base implements IProvider {
protected function generateLegacyCalendarParameter($id, $name) {
return [
'type' => 'calendar',
- 'id' => $id,
+ 'id' => (string)$id,
'name' => $name,
];
}
- /**
- * @param string $uid
- * @return array
- */
- protected function generateUserParameter($uid) {
- if (!isset($this->userDisplayNames[$uid])) {
- $this->userDisplayNames[$uid] = $this->getUserDisplayName($uid);
- }
-
+ protected function generateUserParameter(string $uid): array {
return [
'type' => 'user',
'id' => $uid,
- 'name' => $this->userDisplayNames[$uid],
+ 'name' => $this->userManager->getDisplayName($uid) ?? $uid,
];
}
/**
- * @param string $uid
- * @return string
- */
- protected function getUserDisplayName($uid) {
- $user = $this->userManager->get($uid);
- if ($user instanceof IUser) {
- return $user->getDisplayName();
- }
- return $uid;
- }
-
- /**
* @param string $gid
* @return array
*/
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
index daab7806e46..8c93ddae431 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Calendar.php
@@ -1,31 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * 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;
@@ -48,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
@@ -68,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;
}
/**
@@ -80,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) {
+ 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);
@@ -143,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 96366f54942..87551d7840b 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php
@@ -1,32 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Julius Härtl <jus@bitgrid.net>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * 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;
@@ -40,25 +20,14 @@ use OCP\L10N\IFactory;
class Event extends Base {
public const SUBJECT_OBJECT_ADD = 'object_add';
public const SUBJECT_OBJECT_UPDATE = 'object_update';
+ public const SUBJECT_OBJECT_MOVE = 'object_move';
public const SUBJECT_OBJECT_MOVE_TO_TRASH = 'object_move_to_trash';
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
@@ -68,19 +37,23 @@ 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;
}
/**
* @param array $eventData
* @return array
*/
- protected function generateObjectParameter(array $eventData) {
+ protected function generateObjectParameter(array $eventData, string $affectedUser): array {
if (!isset($eventData['id']) || !isset($eventData['name'])) {
throw new \InvalidArgumentException();
}
@@ -88,23 +61,27 @@ class Event extends Base {
$params = [
'type' => 'calendar-event',
'id' => $eventData['id'],
- 'name' => $eventData['name'],
-
+ 'name' => trim($eventData['name']) !== '' ? $eventData['name'] : $this->l->t('Untitled event'),
];
+
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'];
- $objectId = base64_encode('/remote.php/dav/calendars/' . $linkData['owner'] . '/' . $linkData['calendar_uri'] . '/' . $linkData['object_uri']);
- $link = [
- 'view' => 'dayGridMonth',
- 'timeRange' => 'now',
- 'mode' => 'sidebar',
+ $calendarUri = $this->urlencodeLowerHex($linkData['calendar_uri']);
+ if ($affectedUser === $linkData['owner']) {
+ $objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $linkData['owner'] . '/' . $calendarUri . '/' . $linkData['object_uri']);
+ } else {
+ // Can't use the "real" owner and calendar names here because we create a custom
+ // calendar for incoming shares with the name "<calendar>_shared_by_<sharer>".
+ // Hack: Fix the link by generating it for the incoming shared calendar instead,
+ // 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']);
+ }
+ $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
}
@@ -117,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) {
+ 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);
@@ -145,6 +122,10 @@ class Event extends Base {
$subject = $this->l->t('{actor} updated event {event} in calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_event_self') {
$subject = $this->l->t('You updated event {event} in calendar {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_event') {
+ $subject = $this->l->t('{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_event_self') {
+ $subject = $this->l->t('You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event') {
$subject = $this->l->t('{actor} deleted event {event} from calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self') {
@@ -154,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);
@@ -184,7 +165,7 @@ class Event extends Base {
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
- 'event' => $this->generateClassifiedObjectParameter($parameters['object']),
+ 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_ADD . '_event_self':
case self::SUBJECT_OBJECT_DELETE . '_event_self':
@@ -193,7 +174,25 @@ class Event extends Base {
case self::SUBJECT_OBJECT_RESTORE . '_event_self':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
- 'event' => $this->generateClassifiedObjectParameter($parameters['object']),
+ 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()),
+ ];
+ }
+ }
+
+ if (isset($parameters['sourceCalendar']) && isset($parameters['targetCalendar'])) {
+ switch ($subject) {
+ case self::SUBJECT_OBJECT_MOVE . '_event':
+ return [
+ 'actor' => $this->generateUserParameter($parameters['actor']),
+ 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
+ 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
+ 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()),
+ ];
+ case self::SUBJECT_OBJECT_MOVE . '_event_self':
+ return [
+ 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
+ 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
+ 'event' => $this->generateClassifiedObjectParameter($parameters['object'], $event->getAffectedUser()),
];
}
}
@@ -210,25 +209,37 @@ class Event extends Base {
return [
'actor' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
- 'event' => $this->generateObjectParameter($parameters[2]),
+ 'event' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_ADD . '_event_self':
case self::SUBJECT_OBJECT_DELETE . '_event_self':
case self::SUBJECT_OBJECT_UPDATE . '_event_self':
return [
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
- 'event' => $this->generateObjectParameter($parameters[2]),
+ 'event' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()),
];
}
throw new \InvalidArgumentException();
}
- private function generateClassifiedObjectParameter(array $eventData) {
- $parameter = $this->generateObjectParameter($eventData);
+ private function generateClassifiedObjectParameter(array $eventData, string $affectedUser): array {
+ $parameter = $this->generateObjectParameter($eventData, $affectedUser);
if (!empty($eventData['classified'])) {
$parameter['name'] = $this->l->t('Busy');
}
return $parameter;
}
+
+ /**
+ * Return urlencoded string but with lower cased hex sequences.
+ * The remaining casing will be untouched.
+ */
+ private function urlencodeLowerHex(string $raw): string {
+ return preg_replace_callback(
+ '/%[0-9A-F]{2}/',
+ static fn (array $matches) => strtolower($matches[0]),
+ urlencode($raw),
+ );
+ }
}
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
index a3ab81e38ae..fc0625ec970 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php
@@ -1,29 +1,12 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * 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 {
@@ -33,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) {
+ 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);
@@ -50,27 +33,31 @@ class Todo extends Event {
}
if ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_todo') {
- $subject = $this->l->t('{actor} created todo {todo} in list {calendar}');
+ $subject = $this->l->t('{actor} created to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_todo_self') {
- $subject = $this->l->t('You created todo {todo} in list {calendar}');
+ $subject = $this->l->t('You created to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_todo') {
- $subject = $this->l->t('{actor} deleted todo {todo} from list {calendar}');
+ $subject = $this->l->t('{actor} deleted to-do {todo} from list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_todo_self') {
- $subject = $this->l->t('You deleted todo {todo} from list {calendar}');
+ $subject = $this->l->t('You deleted to-do {todo} from list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo') {
- $subject = $this->l->t('{actor} updated todo {todo} in list {calendar}');
+ $subject = $this->l->t('{actor} updated to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_self') {
- $subject = $this->l->t('You updated todo {todo} in list {calendar}');
+ $subject = $this->l->t('You updated to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_completed') {
- $subject = $this->l->t('{actor} solved todo {todo} in list {calendar}');
+ $subject = $this->l->t('{actor} solved to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self') {
- $subject = $this->l->t('You solved todo {todo} in list {calendar}');
+ $subject = $this->l->t('You solved to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action') {
- $subject = $this->l->t('{actor} reopened todo {todo} in list {calendar}');
+ $subject = $this->l->t('{actor} reopened to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self') {
- $subject = $this->l->t('You reopened todo {todo} in list {calendar}');
+ $subject = $this->l->t('You reopened to-do {todo} in list {calendar}');
+ } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo') {
+ $subject = $this->l->t('{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}');
+ } 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);
@@ -100,7 +87,7 @@ class Todo extends Event {
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
- 'todo' => $this->generateObjectParameter($parameters['object']),
+ 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_ADD . '_todo_self':
case self::SUBJECT_OBJECT_DELETE . '_todo_self':
@@ -109,7 +96,25 @@ class Todo extends Event {
case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
- 'todo' => $this->generateObjectParameter($parameters['object']),
+ 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()),
+ ];
+ }
+ }
+
+ if (isset($parameters['sourceCalendar']) && isset($parameters['targetCalendar'])) {
+ switch ($subject) {
+ case self::SUBJECT_OBJECT_MOVE . '_todo':
+ return [
+ 'actor' => $this->generateUserParameter($parameters['actor']),
+ 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
+ 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
+ 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()),
+ ];
+ case self::SUBJECT_OBJECT_MOVE . '_todo_self':
+ return [
+ 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
+ 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
+ 'todo' => $this->generateObjectParameter($parameters['object'], $event->getAffectedUser()),
];
}
}
@@ -128,7 +133,7 @@ class Todo extends Event {
return [
'actor' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
- 'todo' => $this->generateObjectParameter($parameters[2]),
+ 'todo' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()),
];
case self::SUBJECT_OBJECT_ADD . '_todo_self':
case self::SUBJECT_OBJECT_DELETE . '_todo_self':
@@ -137,7 +142,7 @@ class Todo extends Event {
case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self':
return [
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
- 'todo' => $this->generateObjectParameter($parameters[2]),
+ 'todo' => $this->generateObjectParameter($parameters[2], $event->getAffectedUser()),
];
}
diff --git a/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php b/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
index 20325a253f4..7ab7f16dbbb 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/CalDAVSetting.php
@@ -3,26 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
@@ -30,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 4a226fca439..0ad86a919bc 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Calendar.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
@@ -42,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 0239296a403..ea9476d6f08 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Event.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Event.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
@@ -42,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 7d27b30c4af..ed8377b0ffa 100644
--- a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php
+++ b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Robin Appelman <robin@icewind.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
@@ -38,13 +21,13 @@ class Todo extends CalDAVSetting {
* @since 11.0.0
*/
public function getName() {
- return $this->l->t('A calendar <strong>todo</strong> was modified');
+ return $this->l->t('A calendar <strong>to-do</strong> was modified');
}
/**
* @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
new file mode 100644
index 00000000000..87d26324c32
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
@@ -0,0 +1,194 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV\AppCalendar;
+
+use OCA\DAV\CalDAV\Integration\ExternalCalendar;
+use OCA\DAV\CalDAV\Plugin;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use Sabre\CalDAV\CalendarQueryValidator;
+use Sabre\CalDAV\ICalendarObject;
+use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\PropPatch;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Reader;
+
+class AppCalendar extends ExternalCalendar {
+ protected ICalendar $calendar;
+
+ public function __construct(
+ string $appId,
+ ICalendar $calendar,
+ protected string $principal,
+ ) {
+ parent::__construct($appId, $calendar->getUri());
+ $this->calendar = $calendar;
+ }
+
+ /**
+ * Return permissions supported by the backend calendar
+ * @return int Permissions based on \OCP\Constants
+ */
+ public function getPermissions(): int {
+ // Make sure to only promote write support if the backend implement the correct interface
+ if ($this->calendar instanceof ICreateFromString) {
+ return $this->calendar->getPermissions();
+ }
+ return Constants::PERMISSION_READ;
+ }
+
+ public function getOwner(): ?string {
+ return $this->principal;
+ }
+
+ public function getGroup(): ?string {
+ return null;
+ }
+
+ public function getACL(): array {
+ $acl = [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ]
+ ];
+ if ($this->getPermissions() & Constants::PERMISSION_CREATE) {
+ $acl[] = [
+ 'privilege' => '{DAV:}write',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ];
+ }
+ return $acl;
+ }
+
+ public function setACL(array $acl): void {
+ throw new Forbidden('Setting ACL is not supported on this node');
+ }
+
+ public function getSupportedPrivilegeSet(): ?array {
+ // Use the default one
+ return null;
+ }
+
+ public function getLastModified(): ?int {
+ // unknown
+ return null;
+ }
+
+ public function delete(): void {
+ // No method for deleting a calendar in OCP\Calendar\ICalendar
+ throw new Forbidden('Deleting an entry is not implemented');
+ }
+
+ public function createFile($name, $data = null) {
+ if ($this->calendar instanceof ICreateFromString) {
+ if (is_resource($data)) {
+ $data = stream_get_contents($data) ?: null;
+ }
+ $this->calendar->createFromString($name, is_null($data) ? '' : $data);
+ return null;
+ } else {
+ throw new Forbidden('Creating a new entry is not allowed');
+ }
+ }
+
+ public function getProperties($properties) {
+ return [
+ '{DAV:}displayname' => $this->calendar->getDisplayName() ?: $this->calendar->getKey(),
+ '{http://apple.com/ns/ical/}calendar-color' => $this->calendar->getDisplayColor() ?: '#0082c9',
+ '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT', 'VJOURNAL', 'VTODO']),
+ ];
+ }
+
+ public function calendarQuery(array $filters) {
+ $result = [];
+ $objects = $this->getChildren();
+
+ foreach ($objects as $object) {
+ if ($this->validateFilterForObject($object, $filters)) {
+ $result[] = $object->getName();
+ }
+ }
+
+ return $result;
+ }
+
+ protected function validateFilterForObject(ICalendarObject $object, array $filters): bool {
+ /** @var \Sabre\VObject\Component\VCalendar */
+ $vObject = Reader::read($object->get());
+
+ $validator = new CalendarQueryValidator();
+ $result = $validator->validate($vObject, $filters);
+
+ // Destroy circular references so PHP will GC the object.
+ $vObject->destroy();
+
+ return $result;
+ }
+
+ public function childExists($name): bool {
+ try {
+ $this->getChild($name);
+ return true;
+ } catch (NotFound $error) {
+ return false;
+ }
+ }
+
+ public function getChild($name) {
+ // Try to get calendar by filename
+ $children = $this->calendar->search($name, ['X-FILENAME']);
+ if (count($children) === 0) {
+ // If nothing found try to get by UID from filename
+ $pos = strrpos($name, '.ics');
+ $children = $this->calendar->search(substr($name, 0, $pos ?: null), ['UID']);
+ }
+
+ if (count($children) > 0) {
+ return new CalendarObject($this, $this->calendar, new VCalendar($children));
+ }
+
+ throw new NotFound('Node not found');
+ }
+
+ /**
+ * @return ICalendarObject[]
+ */
+ public function getChildren(): array {
+ $objects = $this->calendar->search('');
+ // We need to group by UID (actually by filename but we do not have that information)
+ $result = [];
+ foreach ($objects as $object) {
+ $uid = (string)$object['UID'] ?: uniqid();
+ if (!isset($result[$uid])) {
+ $result[$uid] = [];
+ }
+ $result[$uid][] = $object;
+ }
+
+ return array_map(function (array $children) {
+ return new CalendarObject($this, $this->calendar, new VCalendar($children));
+ }, $result);
+ }
+
+ public function propPatch(PropPatch $propPatch): void {
+ // no setDisplayColor or setDisplayName in \OCP\Calendar\ICalendar
+ }
+}
diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
new file mode 100644
index 00000000000..72f2ed2c163
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+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;
+use Psr\Log\LoggerInterface;
+
+/* Plugin for wrapping application generated calendars registered in nextcloud core (OCP\Calendar\ICalendarProvider) */
+class AppCalendarPlugin implements ICalendarProvider {
+ public function __construct(
+ protected IManager $manager,
+ protected LoggerInterface $logger,
+ ) {
+ }
+
+ public function getAppID(): string {
+ return 'dav-wrapper';
+ }
+
+ public function fetchAllForCalendarHome(string $principalUri): array {
+ return array_map(function ($calendar) use (&$principalUri) {
+ return new AppCalendar($this->getAppID(), $calendar, $principalUri);
+ }, $this->getWrappedCalendars($principalUri));
+ }
+
+ public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool {
+ return count($this->getWrappedCalendars($principalUri, [ $calendarUri ])) > 0;
+ }
+
+ public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar {
+ $calendars = $this->getWrappedCalendars($principalUri, [ $calendarUri ]);
+ if (count($calendars) > 0) {
+ return new AppCalendar($this->getAppID(), $calendars[0], $principalUri);
+ }
+
+ return null;
+ }
+
+ protected function getWrappedCalendars(string $principalUri, array $calendarUris = []): array {
+ return array_values(
+ array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) {
+ // We must not provide a wrapper for DAV calendars
+ 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
new file mode 100644
index 00000000000..3c62a26df54
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV\AppCalendar;
+
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use Sabre\CalDAV\ICalendarObject;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAVACL\IACL;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Property\ICalendar\DateTime;
+
+class CalendarObject implements ICalendarObject, IACL {
+ public function __construct(
+ private AppCalendar $calendar,
+ private ICalendar|ICreateFromString $backend,
+ private VCalendar $vobject,
+ ) {
+ }
+
+ public function getOwner() {
+ return $this->calendar->getOwner();
+ }
+
+ public function getGroup() {
+ return $this->calendar->getGroup();
+ }
+
+ public function getACL(): array {
+ $acl = [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ]
+ ];
+ if ($this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) {
+ $acl[] = [
+ 'privilege' => '{DAV:}write-content',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ];
+ }
+ return $acl;
+ }
+
+ public function setACL(array $acl): void {
+ throw new Forbidden('Setting ACL is not supported on this node');
+ }
+
+ public function getSupportedPrivilegeSet(): ?array {
+ return null;
+ }
+
+ public function put($data): void {
+ if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) {
+ if (is_resource($data)) {
+ $data = stream_get_contents($data) ?: '';
+ }
+ $this->backend->createFromString($this->getName(), $data);
+ } else {
+ throw new Forbidden('This calendar-object is read-only');
+ }
+ }
+
+ public function get(): string {
+ return $this->vobject->serialize();
+ }
+
+ public function getContentType(): string {
+ return 'text/calendar; charset=utf-8';
+ }
+
+ public function getETag(): ?string {
+ return null;
+ }
+
+ public function getSize() {
+ return mb_strlen($this->vobject->serialize());
+ }
+
+ public function delete(): void {
+ if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_DELETE) {
+ /** @var \Sabre\VObject\Component[] */
+ $components = $this->vobject->getBaseComponents();
+ foreach ($components as $key => $component) {
+ $components[$key]->STATUS = 'CANCELLED';
+ $components[$key]->SEQUENCE = isset($component->SEQUENCE) ? ((int)$component->SEQUENCE->getValue()) + 1 : 1;
+ if ($component->name === 'VEVENT') {
+ $components[$key]->METHOD = 'CANCEL';
+ }
+ }
+ $this->backend->createFromString($this->getName(), (new VCalendar($components))->serialize());
+ } else {
+ throw new Forbidden('This calendar-object is read-only');
+ }
+ }
+
+ public function getName(): string {
+ // Every object is required to have an UID
+ $base = $this->vobject->getBaseComponent();
+ // This should never happen except the app provides invalid calendars (VEvent, VTodo... all require UID to be present)
+ if ($base === null) {
+ throw new NotFound('Invalid node');
+ }
+ if (isset($base->{'X-FILENAME'})) {
+ return (string)$base->{'X-FILENAME'};
+ }
+ return (string)$base->UID . '.ics';
+ }
+
+ public function setName($name): void {
+ throw new Forbidden('This calendar-object is read-only');
+ }
+
+ public function getLastModified(): ?int {
+ $base = $this->vobject->getBaseComponent();
+ if ($base !== null && $this->vobject->getBaseComponent()->{'LAST-MODIFIED'}) {
+ /** @var DateTime */
+ $lastModified = $this->vobject->getBaseComponent()->{'LAST-MODIFIED'};
+ return $lastModified->getDateTime()->getTimestamp();
+ }
+ return null;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php b/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php
index 89e50c7da6b..71b9acb939b 100644
--- a/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php
+++ b/apps/dav/lib/CalDAV/Auth/CustomPrincipalPlugin.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * CalDAV App
- *
- * @copyright 2021 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Auth;
diff --git a/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php b/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php
index 96669558818..ed89638451e 100644
--- a/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php
+++ b/apps/dav/lib/CalDAV/Auth/PublicPrincipalPlugin.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * CalDAV App
- *
- * @copyright 2021 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
- *
- * You should have received a copy of the GNU Affero General Public
- * License along with this library. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Auth;
diff --git a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
index b736d9432bd..681709cdb6f 100644
--- a/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
+++ b/apps/dav/lib/CalDAV/BirthdayCalendar/EnablePlugin.php
@@ -1,32 +1,16 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
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;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
@@ -42,16 +26,6 @@ class EnablePlugin extends ServerPlugin {
public const NS_Nextcloud = 'http://nextcloud.com/ns';
/**
- * @var IConfig
- */
- protected $config;
-
- /**
- * @var BirthdayService
- */
- protected $birthdayService;
-
- /**
* @var Server
*/
protected $server;
@@ -61,10 +35,13 @@ class EnablePlugin extends ServerPlugin {
*
* @param IConfig $config
* @param BirthdayService $birthdayService
+ * @param IUser $user
*/
- public function __construct(IConfig $config, BirthdayService $birthdayService) {
- $this->config = $config;
- $this->birthdayService = $birthdayService;
+ public function __construct(
+ protected IConfig $config,
+ protected BirthdayService $birthdayService,
+ private IUser $user,
+ ) {
}
/**
@@ -117,23 +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;
}
- $principalUri = $node->getOwner();
- $userId = substr($principalUri, 17);
+ $owner = substr($node->getOwner(), 17);
+ if ($owner !== $this->user->getUID()) {
+ $this->server->httpResponse->setStatus(Http::STATUS_FORBIDDEN);
+ return false;
+ }
- $this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
- $this->birthdayService->syncUser($userId);
+ $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 bdcf0796283..680b228766f 100644
--- a/apps/dav/lib/CalDAV/BirthdayService.php
+++ b/apps/dav/lib/CalDAV/BirthdayService.php
@@ -3,32 +3,9 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Achim Königs <garfonso@tratschtante.de>
- * @author Christian Weiske <cweiske@cweiske.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Sven Strickroth <email@cs-ware.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Valdnet <47037905+Valdnet@users.noreply.github.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -53,63 +30,33 @@ use Sabre\VObject\Reader;
*/
class BirthdayService {
public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
-
- /** @var GroupPrincipalBackend */
- private $principalBackend;
-
- /** @var CalDavBackend */
- private $calDavBackEnd;
-
- /** @var CardDavBackend */
- private $cardDavBackEnd;
-
- /** @var IConfig */
- private $config;
-
- /** @var IDBConnection */
- private $dbConnection;
-
- /** @var IL10N */
- private $l10n;
+ public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR';
/**
* BirthdayService constructor.
- *
- * @param CalDavBackend $calDavBackEnd
- * @param CardDavBackend $cardDavBackEnd
- * @param GroupPrincipalBackend $principalBackend
- * @param IConfig $config
- * @param IDBConnection $dbConnection
- * @param IL10N $l10n
*/
- 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,
+ ) {
}
- /**
- * @param int $addressBookId
- * @param string $cardUri
- * @param string $cardData
- */
public function onCardChanged(int $addressBookId,
- string $cardUri,
- string $cardData) {
+ string $cardUri,
+ string $cardData): void {
if (!$this->isGloballyEnabled()) {
return;
}
$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
+ if ($book === null) {
+ return;
+ }
$targetPrincipals[] = $book['principaluri'];
$datesToSync = [
['postfix' => '', 'field' => 'BDAY'],
@@ -122,19 +69,20 @@ class BirthdayService {
continue;
}
+ $reminderOffset = $this->getReminderOffsetForUser($principalUri);
+
$calendar = $this->ensureCalendarExists($principalUri);
+ if ($calendar === null) {
+ return;
+ }
foreach ($datesToSync as $type) {
- $this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type);
+ $this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset);
}
}
}
- /**
- * @param int $addressBookId
- * @param string $cardUri
- */
public function onCardDeleted(int $addressBookId,
- string $cardUri) {
+ string $cardUri): void {
if (!$this->isGloballyEnabled()) {
return;
}
@@ -149,18 +97,16 @@ 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);
}
}
}
/**
- * @param string $principal
- * @return array|null
* @throws \Sabre\DAV\Exception\BadRequest
*/
- public function ensureCalendarExists(string $principal):?array {
+ public function ensureCalendarExists(string $principal): ?array {
$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
if (!is_null($calendar)) {
return $calendar;
@@ -178,12 +124,14 @@ class BirthdayService {
* @param $cardData
* @param $dateField
* @param $postfix
+ * @param $reminderOffset
* @return VCalendar|null
* @throws InvalidDataException
*/
public function buildDateFromContact(string $cardData,
- string $dateField,
- string $postfix):?VCalendar {
+ string $dateField,
+ string $postfix,
+ ?string $reminderOffset):?VCalendar {
if (empty($cardData)) {
return null;
}
@@ -199,6 +147,10 @@ class BirthdayService {
return null;
}
+ if (isset($doc->{self::EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME})) {
+ return null;
+ }
+
if (!isset($doc->{$dateField})) {
return null;
}
@@ -220,33 +172,26 @@ class BirthdayService {
} catch (InvalidDataException $e) {
return null;
}
+ if ($dateParts['year'] !== null) {
+ $parameters = $birthday->parameters();
+ $omitYear = (isset($parameters['X-APPLE-OMIT-YEAR'])
+ && $parameters['X-APPLE-OMIT-YEAR'] === $dateParts['year']);
+ // 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
+ if ($omitYear || (int)$dateParts['year'] === 1604) {
+ $dateParts['year'] = null;
+ }
+ }
- $unknownYear = false;
$originalYear = null;
- if (!$dateParts['year']) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
+ if ($dateParts['year'] !== null) {
+ $originalYear = (int)$dateParts['year'];
+ }
- $unknownYear = true;
- } else {
- $parameters = $birthday->parameters();
- if (isset($parameters['X-APPLE-OMIT-YEAR'])) {
- $omitYear = $parameters['X-APPLE-OMIT-YEAR'];
- if ($dateParts['year'] === $omitYear) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- $unknownYear = true;
- }
- } else {
- $originalYear = (int)$dateParts['year'];
- // 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
- if ($originalYear == 1604) {
- $originalYear = null;
- $unknownYear = true;
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- }
- if ($originalYear < 1970) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- }
- }
+ $leapDay = ((int)$dateParts['month'] === 2
+ && (int)$dateParts['date'] === 29);
+ if ($dateParts['year'] === null || $originalYear < 1970) {
+ $birthday = ($leapDay ? '1972-' : '1970-')
+ . $dateParts['month'] . '-' . $dateParts['date'];
}
try {
@@ -281,18 +226,25 @@ class BirthdayService {
$vEvent->DTEND['VALUE'] = 'DATE';
$vEvent->{'UID'} = $doc->UID . $postfix;
$vEvent->{'RRULE'} = 'FREQ=YEARLY';
+ if ($leapDay) {
+ /* Sabre\VObject supports BYMONTHDAY only if BYMONTH
+ * is also set */
+ $vEvent->{'RRULE'} = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1';
+ }
$vEvent->{'SUMMARY'} = $summary;
$vEvent->{'TRANSP'} = 'TRANSPARENT';
$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
- $vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $unknownYear ? '1' : '0';
+ $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');
+ $alarm->add($vCal->createProperty('TRIGGER', $reminderOffset, ['VALUE' => 'DURATION']));
+ $alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
+ $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
+ $vEvent->add($alarm);
}
- $alarm = $vCal->createComponent('VALARM');
- $alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION']));
- $alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
- $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
- $vEvent->add($alarm);
$vCal->add($vEvent);
return $vCal;
}
@@ -301,8 +253,11 @@ 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
+ }
$calendarObjects = $this->calDavBackEnd->getCalendarObjects($calendar['id'], CalDavBackend::CALENDAR_TYPE_CALENDAR);
foreach ($calendarObjects as $calendarObject) {
@@ -315,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']);
}
}
}
@@ -332,7 +287,7 @@ class BirthdayService {
* @return bool
*/
public function birthdayEvenChanged(string $existingCalendarData,
- VCalendar $newCalendarData):bool {
+ VCalendar $newCalendarData):bool {
try {
$existingBirthday = Reader::read($existingCalendarData);
} catch (Exception $ex) {
@@ -340,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()
);
}
@@ -371,16 +326,18 @@ class BirthdayService {
* @param array $book
* @param int $calendarId
* @param array $type
+ * @param string $reminderOffset
* @throws InvalidDataException
* @throws \Sabre\DAV\Exception\BadRequest
*/
private function updateCalendar(string $cardUri,
- string $cardData,
- array $book,
- int $calendarId,
- array $type):void {
+ string $cardData,
+ array $book,
+ int $calendarId,
+ array $type,
+ ?string $reminderOffset):void {
$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
- $calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix']);
+ $calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix'], $reminderOffset);
$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
if ($calendarData === null) {
if ($existing !== null) {
@@ -421,14 +378,27 @@ class BirthdayService {
}
/**
+ * Extracts the userId part of a principal
+ *
+ * @param string $userPrincipal
+ * @return string|null
+ */
+ private function principalToUserId(string $userPrincipal):?string {
+ if (str_starts_with($userPrincipal, 'principals/users/')) {
+ return substr($userPrincipal, 17);
+ }
+ return null;
+ }
+
+ /**
* Checks if the user opted-out of birthday calendars
*
* @param string $userPrincipal The user principal to check for
* @return bool
*/
private function isUserEnabled(string $userPrincipal):bool {
- if (strpos($userPrincipal, 'principals/users/') === 0) {
- $userId = substr($userPrincipal, 17);
+ $userId = $this->principalToUserId($userPrincipal);
+ if ($userId !== null) {
$isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
return $isEnabled === 'yes';
}
@@ -438,6 +408,23 @@ class BirthdayService {
}
/**
+ * Get the reminder offset value for a user. This is a duration string (e.g.
+ * PT9H) or null if no reminder is wanted.
+ *
+ * @param string $userPrincipal
+ * @return string|null
+ */
+ private function getReminderOffsetForUser(string $userPrincipal):?string {
+ $userId = $this->principalToUserId($userPrincipal);
+ if ($userId !== null) {
+ return $this->config->getUserValue($userId, 'dav', 'birthdayCalendarReminderOffset', 'PT9H') ?: null;
+ }
+
+ // not sure how we got here, just be on the safe side and return the default value
+ return 'PT9H';
+ }
+
+ /**
* Formats title of Birthday event
*
* @param string $field Field name like BDAY, ANNIVERSARY, ...
@@ -447,9 +434,9 @@ class BirthdayService {
* @return string The formatted title
*/
private function formatTitle(string $field,
- string $name,
- int $year = null,
- bool $supports4Byte = true):string {
+ string $name,
+ ?int $year = null,
+ bool $supports4Byte = true):string {
if ($supports4Byte) {
switch ($field) {
case 'BDAY':
diff --git a/apps/dav/lib/CalDAV/CachedSubscription.php b/apps/dav/lib/CalDAV/CachedSubscription.php
index 18e61450ee9..75ee5cb440f 100644
--- a/apps/dav/lib/CalDAV/CachedSubscription.php
+++ b/apps/dav/lib/CalDAV/CachedSubscription.php
@@ -3,41 +3,22 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
-use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\INode;
use Sabre\DAV\PropPatch;
/**
* Class CachedSubscription
*
* @package OCA\DAV\CalDAV
- * @property BackendInterface|CalDavBackend $caldavBackend
+ * @property CalDavBackend $caldavBackend
*/
class CachedSubscription extends \Sabre\CalDAV\Calendar {
@@ -51,7 +32,7 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
/**
* @return array
*/
- public function getACL():array {
+ public function getACL() {
return [
[
'privilege' => '{DAV:}read',
@@ -73,13 +54,18 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
'principal' => '{DAV:}authenticated',
'protected' => true,
],
+ [
+ 'privilege' => '{DAV:}write-properties',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ]
];
}
/**
* @return array
*/
- public function getChildACL():array {
+ public function getChildACL() {
return [
[
'privilege' => '{DAV:}read',
@@ -97,7 +83,6 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
-
];
}
@@ -111,7 +96,7 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
return parent::getOwner();
}
-
+
public function delete() {
$this->caldavBackend->deleteSubscription($this->calendarInfo['id']);
}
@@ -139,9 +124,9 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
}
/**
- * @return array
+ * @return INode[]
*/
- public function getChildren():array {
+ public function getChildren(): array {
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
$children = [];
@@ -169,11 +154,11 @@ class CachedSubscription extends \Sabre\CalDAV\Calendar {
/**
* @param string $name
- * @param null $calendarData
- * @return null|string|void
+ * @param null|resource|string $data
+ * @return null|string
* @throws MethodNotAllowed
*/
- public function createFile($name, $calendarData = null) {
+ public function createFile($name, $data = null) {
throw new MethodNotAllowed('Creating objects in cached subscription is not allowed');
}
diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php
new file mode 100644
index 00000000000..cc1bab6d4fc
--- /dev/null
+++ b/apps/dav/lib/CalDAV/CachedSubscriptionImpl.php
@@ -0,0 +1,102 @@
+<?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\Calendar\ICalendar;
+use OCP\Calendar\ICalendarIsEnabled;
+use OCP\Calendar\ICalendarIsShared;
+use OCP\Calendar\ICalendarIsWritable;
+use OCP\Constants;
+
+class CachedSubscriptionImpl implements ICalendar, ICalendarIsEnabled, ICalendarIsShared, ICalendarIsWritable {
+
+ public function __construct(
+ private CachedSubscription $calendar,
+ /** @var array<string, mixed> */
+ private array $calendarInfo,
+ private CalDavBackend $backend,
+ ) {
+ }
+
+ /**
+ * @return string defining the technical unique key
+ * @since 13.0.0
+ */
+ public function getKey(): string {
+ return (string)$this->calendarInfo['id'];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getUri(): string {
+ return $this->calendarInfo['uri'];
+ }
+
+ /**
+ * In comparison to getKey() this function returns a human readable (maybe translated) name
+ * @since 13.0.0
+ */
+ public function getDisplayName(): ?string {
+ return $this->calendarInfo['{DAV:}displayname'];
+ }
+
+ /**
+ * Calendar color
+ * @since 13.0.0
+ */
+ public function getDisplayColor(): ?string {
+ return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color'];
+ }
+
+ 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);
+ }
+
+ /**
+ * @return int build up using \OCP\Constants
+ * @since 13.0.0
+ */
+ public function getPermissions(): int {
+ $permissions = $this->calendar->getACL();
+ $result = 0;
+ foreach ($permissions as $permission) {
+ switch ($permission['privilege']) {
+ case '{DAV:}read':
+ $result |= Constants::PERMISSION_READ;
+ break;
+ }
+ }
+
+ 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/CachedSubscriptionObject.php b/apps/dav/lib/CalDAV/CachedSubscriptionObject.php
index db8c9fa8e80..dc9141a61b8 100644
--- a/apps/dav/lib/CalDAV/CachedSubscriptionObject.php
+++ b/apps/dav/lib/CalDAV/CachedSubscriptionObject.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -50,7 +33,7 @@ class CachedSubscriptionObject extends \Sabre\CalDAV\CalendarObject {
/**
* @param resource|string $calendarData
- * @return string|void
+ * @return string
* @throws MethodNotAllowed
*/
public function put($calendarData) {
diff --git a/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php b/apps/dav/lib/CalDAV/CachedSubscriptionProvider.php
new file mode 100644
index 00000000000..d64f039d05b
--- /dev/null
+++ b/apps/dav/lib/CalDAV/CachedSubscriptionProvider.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;
+
+use OCP\Calendar\ICalendarProvider;
+
+class CachedSubscriptionProvider implements ICalendarProvider {
+
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ ) {
+ }
+
+ public function getCalendars(string $principalUri, array $calendarUris = []): array {
+ $calendarInfos = $this->calDavBackend->getSubscriptionsForUser($principalUri);
+
+ if (count($calendarUris) > 0) {
+ $calendarInfos = array_filter($calendarInfos, fn (array $subscription) => in_array($subscription['uri'], $calendarUris));
+ }
+
+ $calendarInfos = array_values(array_filter($calendarInfos));
+
+ $iCalendars = [];
+ foreach ($calendarInfos as $calendarInfo) {
+ $calendar = new CachedSubscription($this->calDavBackend, $calendarInfo);
+ $iCalendars[] = new CachedSubscriptionImpl(
+ $calendar,
+ $calendarInfo,
+ $this->calDavBackend,
+ );
+ }
+ return $iCalendars;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php
index f0d332adab5..d5b0d875ede 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -1,48 +1,19 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2018 Georg Ehrke
- * @copyright Copyright (c) 2020, leith abdulla (<online-nextcloud@eleith.com>)
- *
- * @author Chih-Hsuan Yen <yan12125@gmail.com>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author dartcafe <github@dartcafe.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author leith abdulla <online-nextcloud@eleith.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Simon Spannagel <simonspa@kth.se>
- * @author Stefan Weil <sw@weilnetz.de>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vinicius Cubas Brand <vinicius@eita.org.br>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
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;
-use OCA\DAV\DAV\Sharing\Backend;
use OCA\DAV\DAV\Sharing\IShareable;
use OCA\DAV\Events\CachedCalendarObjectCreatedEvent;
use OCA\DAV\Events\CachedCalendarObjectDeletedEvent;
@@ -50,11 +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\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;
@@ -63,16 +29,23 @@ use OCA\DAV\Events\CalendarUpdatedEvent;
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;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IConfig;
use OCP\IDBConnection;
-use OCP\IGroupManager;
-use OCP\ILogger;
-use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
+use Psr\Log\LoggerInterface;
use RuntimeException;
use Sabre\CalDAV\Backend\AbstractBackend;
use Sabre\CalDAV\Backend\SchedulingSupport;
@@ -95,9 +68,10 @@ use Sabre\VObject\ParseException;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use Sabre\VObject\Recur\EventIterator;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
+use Sabre\VObject\Recur\MaxInstancesExceededException;
+use Sabre\VObject\Recur\NoInstancesException;
use function array_column;
+use function array_map;
use function array_merge;
use function array_values;
use function explode;
@@ -117,8 +91,23 @@ 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;
+
public const CALENDAR_TYPE_CALENDAR = 0;
public const CALENDAR_TYPE_SUBSCRIPTION = 1;
@@ -150,7 +139,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @var array
* @psalm-var array<string, string[]>
*/
- public $propertyMap = [
+ public array $propertyMap = [
'{DAV:}displayname' => ['displayname', 'string'],
'{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
'{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
@@ -164,7 +153,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*
* @var array
*/
- public $subscriptionPropertyMap = [
+ public array $subscriptionPropertyMap = [
'{DAV:}displayname' => ['displayname', 'string'],
'{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
'{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
@@ -195,7 +184,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
];
/** @var array parameters to index */
- public static $indexParameters = [
+ public static array $indexParameters = [
'ATTENDEE' => ['CN'],
'ORGANIZER' => ['CN'],
];
@@ -203,86 +192,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* @var string[] Map of uid => display name
*/
- protected $userDisplayNames;
-
- /** @var IDBConnection */
- private $db;
-
- /** @var Backend */
- private $calendarSharingBackend;
-
- /** @var Principal */
- private $principalBackend;
-
- /** @var IUserManager */
- private $userManager;
-
- /** @var ISecureRandom */
- private $random;
+ protected array $userDisplayNames;
- /** @var ILogger */
- private $logger;
+ private string $dbObjectsTable = 'calendarobjects';
+ private string $dbObjectPropertiesTable = 'calendarobjects_props';
+ private string $dbObjectInvitationsTable = 'calendar_invitations';
+ private array $cachedObjects = [];
- /** @var IEventDispatcher */
- private $dispatcher;
-
- /** @var EventDispatcherInterface */
- private $legacyDispatcher;
-
- /** @var IConfig */
- private $config;
-
- /** @var bool */
- private $legacyEndpoint;
-
- /** @var string */
- private $dbObjectPropertiesTable = 'calendarobjects_props';
-
- /**
- * CalDavBackend constructor.
- *
- * @param IDBConnection $db
- * @param Principal $principalBackend
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param ISecureRandom $random
- * @param ILogger $logger
- * @param IEventDispatcher $dispatcher
- * @param EventDispatcherInterface $legacyDispatcher
- * @param bool $legacyEndpoint
- */
- public function __construct(IDBConnection $db,
- Principal $principalBackend,
- IUserManager $userManager,
- IGroupManager $groupManager,
- ISecureRandom $random,
- ILogger $logger,
- IEventDispatcher $dispatcher,
- EventDispatcherInterface $legacyDispatcher,
- IConfig $config,
- bool $legacyEndpoint = false) {
- $this->db = $db;
- $this->principalBackend = $principalBackend;
- $this->userManager = $userManager;
- $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar');
- $this->random = $random;
- $this->logger = $logger;
- $this->dispatcher = $dispatcher;
- $this->legacyDispatcher = $legacyDispatcher;
- $this->config = $config;
- $this->legacyEndpoint = $legacyEndpoint;
+ public function __construct(
+ private IDBConnection $db,
+ private Principal $principalBackend,
+ private IUserManager $userManager,
+ private ISecureRandom $random,
+ private LoggerInterface $logger,
+ private IEventDispatcher $dispatcher,
+ private IConfig $config,
+ private Sharing\Backend $calendarSharingBackend,
+ private bool $legacyEndpoint = false,
+ ) {
}
/**
- * 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('*'))
@@ -305,6 +242,27 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * Return the number of subscriptions for a principal
+ */
+ public function getSubscriptionsForUserCount(string $principalUri): int {
+ $principalUri = $this->convertPrincipal($principalUri, true);
+ $query = $this->db->getQueryBuilder();
+ $query->select($query->func()->count('*'))
+ ->from('calendarsubscriptions');
+
+ if ($principalUri === '') {
+ $query->where($query->expr()->emptyString('principaluri'));
+ } else {
+ $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
+ }
+
+ $result = $query->executeQuery();
+ $column = (int)$result->fetchOne();
+ $result->closeCursor();
+ return $column;
+ }
+
+ /**
* @return array{id: int, deleted_at: int}[]
*/
public function getDeletedCalendars(int $deletedBefore): array {
@@ -314,14 +272,15 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->where($qb->expr()->isNotNull('deleted_at'))
->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
$result = $qb->executeQuery();
- $raw = $result->fetchAll();
- $result->closeCursor();
- return array_map(function ($row) {
- return [
- 'id' => (int) $row['id'],
- 'deleted_at' => (int) $row['deleted_at'],
+ $calendars = [];
+ while (($row = $result->fetch()) !== false) {
+ $calendars[] = [
+ 'id' => (int)$row['id'],
+ 'deleted_at' => (int)$row['deleted_at'],
];
- }, $raw);
+ }
+ $result->closeCursor();
+ return $calendars;
}
/**
@@ -350,132 +309,143 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array
*/
public function getCalendarsForUser($principalUri) {
- $principalUriOriginal = $principalUri;
- $principalUri = $this->convertPrincipal($principalUri, true);
- $fields = array_column($this->propertyMap, 0);
- $fields[] = 'id';
- $fields[] = 'uri';
- $fields[] = 'synctoken';
- $fields[] = 'components';
- $fields[] = 'principaluri';
- $fields[] = 'transparent';
-
- // Making fields a comma-delimited list
- $query = $this->db->getQueryBuilder();
- $query->select($fields)
- ->from('calendars')
- ->orderBy('calendarorder', 'ASC');
-
- if ($principalUri === '') {
- $query->where($query->expr()->emptyString('principaluri'));
- } else {
- $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
- }
-
- $result = $query->executeQuery();
-
- $calendars = [];
- while ($row = $result->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
- $components = [];
- if ($row['components']) {
- $components = explode(',',$row['components']);
- }
-
- $calendar = [
- '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_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),
- ];
-
- $calendar = $this->rowToCalendar($row, $calendar);
- $calendar = $this->addOwnerPrincipalToCalendar($calendar);
- $calendar = $this->addResourceTypeToCalendar($row, $calendar);
+ return $this->atomic(function () use ($principalUri) {
+ $principalUriOriginal = $principalUri;
+ $principalUri = $this->convertPrincipal($principalUri, true);
+ $fields = array_column($this->propertyMap, 0);
+ $fields[] = 'id';
+ $fields[] = 'uri';
+ $fields[] = 'synctoken';
+ $fields[] = 'components';
+ $fields[] = 'principaluri';
+ $fields[] = 'transparent';
+
+ // Making fields a comma-delimited list
+ $query = $this->db->getQueryBuilder();
+ $query->select($fields)
+ ->from('calendars')
+ ->orderBy('calendarorder', 'ASC');
- if (!isset($calendars[$calendar['id']])) {
- $calendars[$calendar['id']] = $calendar;
+ if ($principalUri === '') {
+ $query->where($query->expr()->emptyString('principaluri'));
+ } else {
+ $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
}
- }
- $result->closeCursor();
- // query for shared calendars
- $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
- $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
+ $result = $query->executeQuery();
- $principals[] = $principalUri;
+ $calendars = [];
+ while ($row = $result->fetch()) {
+ $row['principaluri'] = (string)$row['principaluri'];
+ $components = [];
+ if ($row['components']) {
+ $components = explode(',', $row['components']);
+ }
- $fields = array_column($this->propertyMap, 0);
- $fields[] = 'a.id';
- $fields[] = 'a.uri';
- $fields[] = 'a.synctoken';
- $fields[] = 'a.components';
- $fields[] = 'a.principaluri';
- $fields[] = 'a.transparent';
- $fields[] = 's.access';
- $query = $this->db->getQueryBuilder();
- $query->select($fields)
- ->from('dav_shares', 's')
- ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
- ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
- ->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
- ->setParameter('type', 'calendar')
- ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY);
+ $calendar = [
+ 'id' => $row['id'],
+ 'uri' => $row['uri'],
+ 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
+ '{' . 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),
+ ];
- $result = $query->executeQuery();
+ $calendar = $this->rowToCalendar($row, $calendar);
+ $calendar = $this->addOwnerPrincipalToCalendar($calendar);
+ $calendar = $this->addResourceTypeToCalendar($row, $calendar);
- $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
- while ($row = $result->fetch()) {
- $row['principaluri'] = (string) $row['principaluri'];
- if ($row['principaluri'] === $principalUri) {
- continue;
+ if (!isset($calendars[$calendar['id']])) {
+ $calendars[$calendar['id']] = $calendar;
+ }
}
+ $result->closeCursor();
- $readOnly = (int) $row['access'] === Backend::ACCESS_READ;
- if (isset($calendars[$row['id']])) {
- if ($readOnly) {
- // New share can not have more permissions then the old one.
+ // query for shared calendars
+ $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
+ $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
+ $principals[] = $principalUri;
+
+ $fields = array_column($this->propertyMap, 0);
+ $fields = array_map(function (string $field) {
+ return 'a.' . $field;
+ }, $fields);
+ $fields[] = 'a.id';
+ $fields[] = 'a.uri';
+ $fields[] = 'a.synctoken';
+ $fields[] = 'a.components';
+ $fields[] = 'a.principaluri';
+ $fields[] = 'a.transparent';
+ $fields[] = 's.access';
+
+ $select = $this->db->getQueryBuilder();
+ $subSelect = $this->db->getQueryBuilder();
+
+ $subSelect->select('resourceid')
+ ->from('dav_shares', 'd')
+ ->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
+ ->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY));
+
+ $select->select($fields)
+ ->from('dav_shares', 's')
+ ->join('s', 'calendars', 'a', $select->expr()->eq('s.resourceid', 'a.id', IQueryBuilder::PARAM_INT))
+ ->where($select->expr()->in('s.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
+ ->andWhere($select->expr()->eq('s.type', $select->createNamedParameter('calendar', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
+ ->andWhere($select->expr()->notIn('a.id', $select->createFunction($subSelect->getSQL()), IQueryBuilder::PARAM_INT_ARRAY));
+
+ $results = $select->executeQuery();
+
+ $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
+ while ($row = $results->fetch()) {
+ $row['principaluri'] = (string)$row['principaluri'];
+ if ($row['principaluri'] === $principalUri) {
continue;
}
- if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
- $calendars[$row['id']][$readOnlyPropertyName] === 0) {
- // Old share is already read-write, no more permissions can be gained
- continue;
+
+ $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) {
+ // Old share is already read-write, no more permissions can be gained
+ continue;
+ }
}
- }
- [, $name] = Uri\split($row['principaluri']);
- $uri = $row['uri'] . '_shared_by_' . $name;
- $row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
- $components = [];
- if ($row['components']) {
- $components = explode(',',$row['components']);
- }
- $calendar = [
- '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_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),
- $readOnlyPropertyName => $readOnly,
- ];
+ [, $name] = Uri\split($row['principaluri']);
+ $uri = $row['uri'] . '_shared_by_' . $name;
+ $row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')';
+ $components = [];
+ if ($row['components']) {
+ $components = explode(',', $row['components']);
+ }
+ $calendar = [
+ 'id' => $row['id'],
+ 'uri' => $uri,
+ 'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
+ '{' . 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),
+ $readOnlyPropertyName => $readOnly,
+ ];
- $calendar = $this->rowToCalendar($row, $calendar);
- $calendar = $this->addOwnerPrincipalToCalendar($calendar);
- $calendar = $this->addResourceTypeToCalendar($row, $calendar);
+ $calendar = $this->rowToCalendar($row, $calendar);
+ $calendar = $this->addOwnerPrincipalToCalendar($calendar);
+ $calendar = $this->addResourceTypeToCalendar($row, $calendar);
- $calendars[$calendar['id']] = $calendar;
- }
- $result->closeCursor();
+ $calendars[$calendar['id']] = $calendar;
+ }
+ $result->closeCursor();
- return array_values($calendars);
+ return array_values($calendars);
+ }, $this->db);
}
/**
@@ -499,17 +469,17 @@ 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']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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'),
];
@@ -526,25 +496,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return array_values($calendars);
}
-
- /**
- * @param $uid
- * @return string
- */
- private function getUserDisplayName($uid) {
- if (!isset($this->userDisplayNames[$uid])) {
- $user = $this->userManager->get($uid);
-
- if ($user instanceof IUser) {
- $this->userDisplayNames[$uid] = $user->getDisplayName();
- } else {
- $this->userDisplayNames[$uid] = $uid;
- }
- }
-
- return $this->userDisplayNames[$uid];
- }
-
/**
* @return array
*/
@@ -568,19 +519,19 @@ 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 = [];
if ($row['components']) {
- $components = explode(',',$row['components']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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),
@@ -633,19 +584,19 @@ 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 = [];
if ($row['components']) {
- $components = explode(',',$row['components']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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),
@@ -688,18 +639,18 @@ 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']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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'),
];
@@ -712,10 +663,10 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * @param $calendarId
+ * @psalm-return CalendarInfo|null
* @return array|null
*/
- public function getCalendarById($calendarId) {
+ public function getCalendarById(int $calendarId): ?array {
$fields = array_column($this->propertyMap, 0);
$fields[] = 'id';
$fields[] = 'uri';
@@ -737,18 +688,18 @@ 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']);
+ $components = explode(',', $row['components']);
}
$calendar = [
'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'),
];
@@ -785,7 +736,44 @@ 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'],
+ '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);
+ }
+
+ 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'],
@@ -793,7 +781,7 @@ 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);
@@ -809,14 +797,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param string $calendarUri
* @param array $properties
* @return int
+ *
+ * @throws CalendarException
*/
public function createCalendar($principalUri, $calendarUri, array $properties) {
+ if (strlen($calendarUri) > 255) {
+ throw new CalendarException('URI too long. Calendar not created');
+ }
+
$values = [
'principaluri' => $this->convertPrincipal($principalUri, true),
'uri' => $calendarUri,
'synctoken' => 1,
'transparent' => 0,
- 'components' => 'VEVENT,VTODO',
+ 'components' => 'VEVENT,VTODO,VJOURNAL',
'displayname' => $calendarUri
];
@@ -826,7 +820,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
}
- $values['components'] = implode(',',$properties[$sccs]->getValue());
+ $values['components'] = implode(',', $properties[$sccs]->getValue());
} elseif (isset($properties['components'])) {
// Allow to provide components internally without having
// to create a SupportedCalendarComponentSet object
@@ -835,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]) {
@@ -844,15 +838,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
- $query = $this->db->getQueryBuilder();
- $query->insert('calendars');
- foreach ($values as $column => $value) {
- $query->setValue($column, $query->createNamedParameter($value));
- }
- $query->executeStatement();
- $calendarId = $query->getLastInsertId();
+ [$calendarId, $calendarData] = $this->atomic(function () use ($values) {
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendars');
+ foreach ($values as $column => $value) {
+ $query->setValue($column, $query->createNamedParameter($value));
+ }
+ $query->executeStatement();
+ $calendarId = $query->getLastInsertId();
+
+ $calendarData = $this->getCalendarById($calendarId);
+ return [$calendarId, $calendarData];
+ }, $this->db);
- $calendarData = $this->getCalendarById($calendarId);
$this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
return $calendarId;
@@ -884,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];
@@ -892,19 +890,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
break;
}
}
- $query = $this->db->getQueryBuilder();
- $query->update('calendars');
- foreach ($newValues as $fieldName => $value) {
- $query->set($fieldName, $query->createNamedParameter($value));
- }
- $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
- $query->executeStatement();
+ [$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendars');
+ foreach ($newValues as $fieldName => $value) {
+ $query->set($fieldName, $query->createNamedParameter($value));
+ }
+ $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
+ $query->executeStatement();
- $this->addChange($calendarId, "", 2);
+ $this->addChanges($calendarId, [''], 2);
- $calendarData = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
- $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations));
+ $calendarData = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
+ return [$calendarData, $shares];
+ }, $this->db);
+
+ $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
return true;
});
@@ -917,81 +919,162 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
- // 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.
- $calendarData = $this->getCalendarById($calendarId);
- $isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
- $trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
- if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
+ $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.
$calendarData = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
+ $isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
+ $trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
+ if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
+ $calendarData = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
- $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
- $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
- ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
- ->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
- ->executeStatement();
+ $this->purgeCalendarInvitations($calendarId);
- $qbDeleteCalendarObjects = $this->db->getQueryBuilder();
- $qbDeleteCalendarObjects->delete('calendarobjects')
- ->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
- ->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
- ->executeStatement();
+ $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
+ $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
+ ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
+ ->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
+ ->executeStatement();
- $qbDeleteCalendarChanges = $this->db->getQueryBuilder();
- $qbDeleteCalendarObjects->delete('calendarchanges')
- ->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
- ->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
- ->executeStatement();
+ $qbDeleteCalendarObjects = $this->db->getQueryBuilder();
+ $qbDeleteCalendarObjects->delete('calendarobjects')
+ ->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
+ ->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
+ ->executeStatement();
- $this->calendarSharingBackend->deleteAllShares($calendarId);
+ $qbDeleteCalendarChanges = $this->db->getQueryBuilder();
+ $qbDeleteCalendarChanges->delete('calendarchanges')
+ ->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
+ ->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
+ ->executeStatement();
- $qbDeleteCalendar = $this->db->getQueryBuilder();
- $qbDeleteCalendarObjects->delete('calendars')
- ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
- ->executeStatement();
+ $this->calendarSharingBackend->deleteAllShares($calendarId);
- // Only dispatch if we actually deleted anything
- if ($calendarData) {
- $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares));
- }
- } else {
- $qbMarkCalendarDeleted = $this->db->getQueryBuilder();
- $qbMarkCalendarDeleted->update('calendars')
- ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
- ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
- ->executeStatement();
+ $qbDeleteCalendar = $this->db->getQueryBuilder();
+ $qbDeleteCalendar->delete('calendars')
+ ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
+ ->executeStatement();
- $calendarData = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
- if ($calendarData) {
- $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
- (int)$calendarId,
- $calendarData,
- $shares
- ));
+ // Only dispatch if we actually deleted anything
+ if ($calendarData) {
+ $this->dispatcher->dispatchTyped(new CalendarDeletedEvent($calendarId, $calendarData, $shares));
+ }
+ } else {
+ $qbMarkCalendarDeleted = $this->db->getQueryBuilder();
+ $qbMarkCalendarDeleted->update('calendars')
+ ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
+ ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
+ ->executeStatement();
+
+ $calendarData = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
+ if ($calendarData) {
+ $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
+ $calendarId,
+ $calendarData,
+ $shares
+ ));
+ }
}
- }
+ }, $this->db);
}
public function restoreCalendar(int $id): void {
+ $this->atomic(function () use ($id): void {
+ $qb = $this->db->getQueryBuilder();
+ $update = $qb->update('calendars')
+ ->set('deleted_at', $qb->createNamedParameter(null))
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ $update->executeStatement();
+
+ $calendarData = $this->getCalendarById($id);
+ $shares = $this->getShares($id);
+ if ($calendarData === null) {
+ throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
+ }
+ $this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
+ $id,
+ $calendarData,
+ $shares
+ ));
+ }, $this->db);
+ }
+
+ /**
+ * 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();
- $update = $qb->update('calendars')
- ->set('deleted_at', $qb->createNamedParameter(null))
- ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
- $update->executeStatement();
+ $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();
- $calendarData = $this->getCalendarById($id);
- $shares = $this->getShares($id);
- if ($calendarData === null) {
- throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
+ $result = [];
+ while (($row = $stmt->fetch()) !== false) {
+ $result[$row['uid']] = [
+ 'id' => $row['id'],
+ 'etag' => $row['etag'],
+ 'uri' => $row['uri'],
+ 'calendardata' => $row['calendardata'],
+ ];
}
- $this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
- $id,
- $calendarData,
- $shares
- ));
+ $stmt->closeCursor();
+
+ return $result;
}
/**
@@ -1046,7 +1129,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$stmt = $query->executeQuery();
$result = [];
- foreach ($stmt->fetchAll() as $row) {
+ while (($row = $stmt->fetch()) !== false) {
$result[] = [
'id' => $row['id'],
'uri' => $row['uri'],
@@ -1073,18 +1156,18 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$stmt = $query->executeQuery();
$result = [];
- foreach ($stmt->fetchAll() as $row) {
+ while (($row = $stmt->fetch()) !== false) {
$result[] = [
'id' => $row['id'],
'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();
@@ -1123,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();
@@ -1149,8 +1232,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array|null
*/
public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
+ $key = $calendarId . '::' . $objectUri . '::' . $calendarType;
+ if (isset($this->cachedObjects[$key])) {
+ return $this->cachedObjects[$key];
+ }
$query = $this->db->getQueryBuilder();
- $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
+ $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)))
@@ -1163,16 +1250,24 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
return null;
}
+ $object = $this->rowToCalendarObject($row);
+ $this->cachedObjects[$key] = $object;
+ return $object;
+ }
+
+ private function rowToCalendarObject(array $row): array {
return [
'id' => $row['id'],
'uri' => $row['uri'],
+ 'uid' => $row['uid'],
'lastmodified' => $row['lastmodified'],
'etag' => '"' . $row['etag'] . '"',
'calendarid' => $row['calendarid'],
'size' => (int)$row['size'],
'calendardata' => $this->readBlob($row['calendardata']),
'component' => strtolower($row['componenttype']),
- 'classification' => (int)$row['classification']
+ 'classification' => (int)$row['classification'],
+ '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
];
}
@@ -1248,81 +1343,79 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return string
*/
public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
+ $this->cachedObjects = [];
$extraData = $this->getDenormalizedData($calendarData);
- // Try to detect duplicates
- $qb = $this->db->getQueryBuilder();
- $qb->select($qb->func()->count('*'))
- ->from('calendarobjects')
- ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
- ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
- ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
- ->andWhere($qb->expr()->isNull('deleted_at'));
- $result = $qb->executeQuery();
- $count = (int) $result->fetchOne();
- $result->closeCursor();
+ return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
+ // Try to detect duplicates
+ $qb = $this->db->getQueryBuilder();
+ $qb->select($qb->func()->count('*'))
+ ->from('calendarobjects')
+ ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
+ ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
+ ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
+ ->andWhere($qb->expr()->isNull('deleted_at'));
+ $result = $qb->executeQuery();
+ $count = (int)$result->fetchOne();
+ $result->closeCursor();
- if ($count !== 0) {
- throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
- }
- // For a more specific error message we also try to explicitly look up the UID but as a deleted entry
- $qbDel = $this->db->getQueryBuilder();
- $qbDel->select($qb->func()->count('*'))
- ->from('calendarobjects')
- ->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
- ->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
- ->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
- ->andWhere($qbDel->expr()->isNotNull('deleted_at'));
- $result = $qbDel->executeQuery();
- $count = (int) $result->fetchOne();
- $result->closeCursor();
- if ($count !== 0) {
- throw new BadRequest('Deleted calendar object with uid already exists in this calendar collection.');
- }
+ if ($count !== 0) {
+ throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
+ }
+ // For a more specific error message we also try to explicitly look up the UID but as a deleted entry
+ $qbDel = $this->db->getQueryBuilder();
+ $qbDel->select('*')
+ ->from('calendarobjects')
+ ->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
+ ->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
+ ->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
+ ->andWhere($qbDel->expr()->isNotNull('deleted_at'));
+ $result = $qbDel->executeQuery();
+ $found = $result->fetch();
+ $result->closeCursor();
+ if ($found !== false) {
+ // the object existed previously but has been deleted
+ // remove the trashbin entry and continue as if it was a new object
+ $this->deleteCalendarObject($calendarId, $found['uri']);
+ }
- $query = $this->db->getQueryBuilder();
- $query->insert('calendarobjects')
- ->values([
- 'calendarid' => $query->createNamedParameter($calendarId),
- 'uri' => $query->createNamedParameter($objectUri),
- 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
- 'lastmodified' => $query->createNamedParameter(time()),
- 'etag' => $query->createNamedParameter($extraData['etag']),
- 'size' => $query->createNamedParameter($extraData['size']),
- 'componenttype' => $query->createNamedParameter($extraData['componentType']),
- 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
- 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
- 'classification' => $query->createNamedParameter($extraData['classification']),
- 'uid' => $query->createNamedParameter($extraData['uid']),
- 'calendartype' => $query->createNamedParameter($calendarType),
- ])
- ->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendarobjects')
+ ->values([
+ 'calendarid' => $query->createNamedParameter($calendarId),
+ 'uri' => $query->createNamedParameter($objectUri),
+ 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
+ 'lastmodified' => $query->createNamedParameter(time()),
+ 'etag' => $query->createNamedParameter($extraData['etag']),
+ 'size' => $query->createNamedParameter($extraData['size']),
+ 'componenttype' => $query->createNamedParameter($extraData['componentType']),
+ 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
+ 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
+ 'classification' => $query->createNamedParameter($extraData['classification']),
+ 'uid' => $query->createNamedParameter($extraData['uid']),
+ 'calendartype' => $query->createNamedParameter($calendarType),
+ ])
+ ->executeStatement();
- $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
- $this->addChange($calendarId, $objectUri, 1, $calendarType);
+ $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
+ $this->addChanges($calendarId, [$objectUri], 1, $calendarType);
- $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
- if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
- $calendarRow = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
+ $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
+ assert($objectRow !== null);
- $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
- } else {
- $subscriptionRow = $this->getSubscriptionById($calendarId);
-
- $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject',
- [
- 'subscriptionId' => $calendarId,
- 'calendarData' => $subscriptionRow,
- 'shares' => [],
- 'objectData' => $objectRow,
- ]
- ));
- }
+ if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
+ $calendarRow = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
+
+ $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow));
+ } else {
+ $subscriptionRow = $this->getSubscriptionById($calendarId);
- return '"' . $extraData['etag'] . '"';
+ $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
+ }
+
+ return '"' . $extraData['etag'] . '"';
+ }, $this->db);
}
/**
@@ -1345,9 +1438,12 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return string
*/
public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
+ $this->cachedObjects = [];
$extraData = $this->getDenormalizedData($calendarData);
- $query = $this->db->getQueryBuilder();
- $query->update('calendarobjects')
+
+ 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']))
@@ -1357,105 +1453,88 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
->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)))
- ->executeStatement();
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
+ ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
+ ->executeStatement();
- $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
- $this->addChange($calendarId, $objectUri, 2, $calendarType);
+ $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
+ $this->addChanges($calendarId, [$objectUri], 2, $calendarType);
- $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
- if (is_array($objectRow)) {
- if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
- $calendarRow = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
+ $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
+ if (is_array($objectRow)) {
+ if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
+ $calendarRow = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
- $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow));
- } else {
- $subscriptionRow = $this->getSubscriptionById($calendarId);
+ $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow));
+ } else {
+ $subscriptionRow = $this->getSubscriptionById($calendarId);
- $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject',
- [
- 'subscriptionId' => $calendarId,
- 'calendarData' => $subscriptionRow,
- 'shares' => [],
- 'objectData' => $objectRow,
- ]
- ));
+ $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow));
+ }
}
- }
- return '"' . $extraData['etag'] . '"';
+ return '"' . $extraData['etag'] . '"';
+ }, $this->db);
}
/**
* 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 $principalUri
+ * @param string $tragetObjectUri
* @param int $calendarType
* @return bool
* @throws Exception
*/
- public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $principalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
- $object = $this->getCalendarObjectById($principalUri, $objectId);
- if (empty($object)) {
- return false;
- }
-
- $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))
- ->executeStatement();
+ 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 ($sourcePrincipalUri, $sourceObjectId, $targetPrincipalUri, $targetCalendarId, $tragetObjectUri, $calendarType) {
+ $object = $this->getCalendarObjectById($sourcePrincipalUri, $sourceObjectId);
+ if (empty($object)) {
+ return false;
+ }
- $this->purgeProperties($sourceCalendarId, $objectId);
- $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
+ $sourceCalendarId = $object['calendarid'];
+ $sourceObjectUri = $object['uri'];
- $this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType);
- $this->addChange($targetCalendarId, $object['uri'], 3, $calendarType);
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendarobjects')
+ ->set('calendarid', $query->createNamedParameter($targetCalendarId, 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();
- $object = $this->getCalendarObjectById($principalUri, $objectId);
- // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
- if (empty($object)) {
- return false;
- }
+ $this->purgeProperties($sourceCalendarId, $sourceObjectId);
+ $this->updateProperties($targetCalendarId, $tragetObjectUri, $object['calendardata'], $calendarType);
- $calendarRow = $this->getCalendarById($targetCalendarId);
- // the calendar this event is being moved to does not exist any longer
- if (empty($calendarRow)) {
- return false;
- }
+ $this->addChanges($sourceCalendarId, [$sourceObjectUri], 3, $calendarType);
+ $this->addChanges($targetCalendarId, [$tragetObjectUri], 1, $calendarType);
- if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
- $shares = $this->getShares($targetCalendarId);
- $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($targetCalendarId, $calendarRow, $shares, $object));
- }
- return true;
- }
+ $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;
+ }
+ $targetCalendarRow = $this->getCalendarById($targetCalendarId);
+ // the calendar this event is being moved to does not exist any longer
+ if (empty($targetCalendarRow)) {
+ return false;
+ }
- /**
- * @param int $calendarObjectId
- * @param int $classification
- */
- public function setClassification($calendarObjectId, $classification) {
- 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();
+ if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
+ $sourceShares = $this->getShares($sourceCalendarId);
+ $targetShares = $this->getShares($targetCalendarId);
+ $sourceCalendarRow = $this->getCalendarById($sourceCalendarId);
+ $this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object));
+ }
+ return true;
+ }, $this->db);
}
/**
@@ -1470,86 +1549,82 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
- $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
+ $this->cachedObjects = [];
+ $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
+ $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
- if ($data === null) {
- // Nothing to delete
- return;
- }
+ if ($data === null) {
+ // Nothing to delete
+ return;
+ }
- if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
- $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
- $stmt->execute([$calendarId, $objectUri, $calendarType]);
+ if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
+ $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
+ $stmt->execute([$calendarId, $objectUri, $calendarType]);
- $this->purgeProperties($calendarId, $data['id']);
+ $this->purgeProperties($calendarId, $data['id']);
- if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
- $calendarRow = $this->getCalendarById($calendarId);
- $shares = $this->getShares($calendarId);
+ $this->purgeObjectInvitations($data['uid']);
- $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data));
- } else {
- $subscriptionRow = $this->getSubscriptionById($calendarId);
+ if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
+ $calendarRow = $this->getCalendarById($calendarId);
+ $shares = $this->getShares($calendarId);
- $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject',
- [
- 'subscriptionId' => $calendarId,
- 'calendarData' => $subscriptionRow,
- 'shares' => [],
- 'objectData' => $data,
- ]
- ));
- }
- } else {
- $pathInfo = pathinfo($data['uri']);
- if (!empty($pathInfo['extension'])) {
- // Append a suffix to "free" the old URI for recreation
- $newUri = sprintf(
- "%s-deleted.%s",
- $pathInfo['filename'],
- $pathInfo['extension']
- );
- } else {
- $newUri = sprintf(
- "%s-deleted",
- $pathInfo['filename']
- );
- }
+ $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data));
+ } else {
+ $subscriptionRow = $this->getSubscriptionById($calendarId);
- // Try to detect conflicts before the DB does
- // As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
- $newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
- if ($newObject !== null) {
- throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
- }
+ $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data));
+ }
+ } else {
+ $pathInfo = pathinfo($data['uri']);
+ if (!empty($pathInfo['extension'])) {
+ // Append a suffix to "free" the old URI for recreation
+ $newUri = sprintf(
+ '%s-deleted.%s',
+ $pathInfo['filename'],
+ $pathInfo['extension']
+ );
+ } else {
+ $newUri = sprintf(
+ '%s-deleted',
+ $pathInfo['filename']
+ );
+ }
- $qb = $this->db->getQueryBuilder();
- $markObjectDeletedQuery = $qb->update('calendarobjects')
- ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
- ->set('uri', $qb->createNamedParameter($newUri))
- ->where(
- $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
- $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
- $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
- );
- $markObjectDeletedQuery->executeStatement();
+ // Try to detect conflicts before the DB does
+ // As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
+ $newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
+ if ($newObject !== null) {
+ throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
+ }
- $calendarData = $this->getCalendarById($calendarId);
- if ($calendarData !== null) {
- $this->dispatcher->dispatchTyped(
- new CalendarObjectMovedToTrashEvent(
- (int)$calendarId,
- $calendarData,
- $this->getShares($calendarId),
- $data
- )
- );
+ $qb = $this->db->getQueryBuilder();
+ $markObjectDeletedQuery = $qb->update('calendarobjects')
+ ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
+ ->set('uri', $qb->createNamedParameter($newUri))
+ ->where(
+ $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
+ $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
+ $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
+ );
+ $markObjectDeletedQuery->executeStatement();
+
+ $calendarData = $this->getCalendarById($calendarId);
+ if ($calendarData !== null) {
+ $this->dispatcher->dispatchTyped(
+ new CalendarObjectMovedToTrashEvent(
+ $calendarId,
+ $calendarData,
+ $this->getShares($calendarId),
+ $data
+ )
+ );
+ }
}
- }
- $this->addChange($calendarId, $objectUri, 3, $calendarType);
+ $this->addChanges($calendarId, [$objectUri], 3, $calendarType);
+ }, $this->db);
}
/**
@@ -1558,50 +1633,53 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @throws Forbidden
*/
public function restoreCalendarObject(array $objectData): void {
- $id = (int) $objectData['id'];
- $restoreUri = str_replace("-deleted.ics", ".ics", $objectData['uri']);
- $targetObject = $this->getCalendarObject(
- $objectData['calendarid'],
- $restoreUri
- );
- if ($targetObject !== null) {
- throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
- }
+ $this->cachedObjects = [];
+ $this->atomic(function () use ($objectData): void {
+ $id = (int)$objectData['id'];
+ $restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']);
+ $targetObject = $this->getCalendarObject(
+ $objectData['calendarid'],
+ $restoreUri
+ );
+ if ($targetObject !== null) {
+ throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
+ }
- $qb = $this->db->getQueryBuilder();
- $update = $qb->update('calendarobjects')
- ->set('uri', $qb->createNamedParameter($restoreUri))
- ->set('deleted_at', $qb->createNamedParameter(null))
- ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
- $update->executeStatement();
-
- // Make sure this change is tracked in the changes table
- $qb2 = $this->db->getQueryBuilder();
- $selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
- ->selectAlias('componenttype', 'component')
- ->from('calendarobjects')
- ->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
- $result = $selectObject->executeQuery();
- $row = $result->fetch();
- $result->closeCursor();
- if ($row === false) {
- // Welp, this should possibly not have happened, but let's ignore
- return;
- }
- $this->addChange($row['calendarid'], $row['uri'], 1, (int) $row['calendartype']);
+ $qb = $this->db->getQueryBuilder();
+ $update = $qb->update('calendarobjects')
+ ->set('uri', $qb->createNamedParameter($restoreUri))
+ ->set('deleted_at', $qb->createNamedParameter(null))
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ $update->executeStatement();
+
+ // Make sure this change is tracked in the changes table
+ $qb2 = $this->db->getQueryBuilder();
+ $selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
+ ->selectAlias('componenttype', 'component')
+ ->from('calendarobjects')
+ ->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ $result = $selectObject->executeQuery();
+ $row = $result->fetch();
+ $result->closeCursor();
+ if ($row === false) {
+ // Welp, this should possibly not have happened, but let's ignore
+ return;
+ }
+ $this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']);
- $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'],
- $calendarRow,
- $this->getShares((int) $row['calendarid']),
- $row
- )
- );
+ $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'],
+ $calendarRow,
+ $this->getShares((int)$row['calendarid']),
+ $row
+ )
+ );
+ }, $this->db);
}
/**
@@ -1644,7 +1722,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* Note that especially time-range-filters may be difficult to parse. A
* time-range filter specified on a VEVENT must for instance also handle
* recurrence rules correctly.
- * A good example of how to interprete all these filters can also simply
+ * A good example of how to interpret all these filters can also simply
* be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
* as possible, so it gives you a good idea on what type of stuff you need
* to think of.
@@ -1683,12 +1761,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
}
- $columns = ['uri'];
- if ($requirePostFilter) {
- $columns = ['uri', 'calendardata'];
- }
$query = $this->db->getQueryBuilder();
- $query->select($columns)
+ $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)))
@@ -1709,21 +1783,32 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$result = [];
while ($row = $stmt->fetch()) {
+ // if we leave it as a blob we can't read it both from the post filter and the rowToCalendarObject
+ if (isset($row['calendardata'])) {
+ $row['calendardata'] = $this->readBlob($row['calendardata']);
+ }
+
if ($requirePostFilter) {
// validateFilterForObject will parse the calendar data
// catch parsing errors
try {
$matches = $this->validateFilterForObject($row, $filters);
} catch (ParseException $ex) {
- $this->logger->logException($ex, [
+ $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
'app' => 'dav',
- 'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
+ 'exception' => $ex,
]);
continue;
} catch (InvalidDataException $ex) {
- $this->logger->logException($ex, [
+ $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',
- 'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri']
+ 'exception' => $ex,
]);
continue;
}
@@ -1733,6 +1818,8 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
$result[] = $row['uri'];
+ $key = $calendarId . '::' . $row['uri'] . '::' . $calendarType;
+ $this->cachedObjects[$key] = $this->rowToCalendarObject($row);
}
return $result;
@@ -1750,118 +1837,120 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array
*/
public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
- $calendars = $this->getCalendarsForUser($principalUri);
- $ownCalendars = [];
- $sharedCalendars = [];
+ return $this->atomic(function () use ($principalUri, $filters, $limit, $offset) {
+ $calendars = $this->getCalendarsForUser($principalUri);
+ $ownCalendars = [];
+ $sharedCalendars = [];
- $uriMapper = [];
+ $uriMapper = [];
- foreach ($calendars as $calendar) {
- if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
- $ownCalendars[] = $calendar['id'];
- } else {
- $sharedCalendars[] = $calendar['id'];
+ foreach ($calendars as $calendar) {
+ if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
+ $ownCalendars[] = $calendar['id'];
+ } else {
+ $sharedCalendars[] = $calendar['id'];
+ }
+ $uriMapper[$calendar['id']] = $calendar['uri'];
+ }
+ if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
+ return [];
}
- $uriMapper[$calendar['id']] = $calendar['uri'];
- }
- if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
- return [];
- }
- $query = $this->db->getQueryBuilder();
- // Calendar id expressions
- $calendarExpressions = [];
- foreach ($ownCalendars as $id) {
- $calendarExpressions[] = $query->expr()->andX(
- $query->expr()->eq('c.calendarid',
- $query->createNamedParameter($id)),
- $query->expr()->eq('c.calendartype',
+ $query = $this->db->getQueryBuilder();
+ // Calendar id expressions
+ $calendarExpressions = [];
+ foreach ($ownCalendars as $id) {
+ $calendarExpressions[] = $query->expr()->andX(
+ $query->expr()->eq('c.calendarid',
+ $query->createNamedParameter($id)),
+ $query->expr()->eq('c.calendartype',
$query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
- }
- foreach ($sharedCalendars as $id) {
- $calendarExpressions[] = $query->expr()->andX(
- $query->expr()->eq('c.calendarid',
- $query->createNamedParameter($id)),
- $query->expr()->eq('c.classification',
- $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
- $query->expr()->eq('c.calendartype',
- $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
- }
-
- if (count($calendarExpressions) === 1) {
- $calExpr = $calendarExpressions[0];
- } else {
- $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
- }
+ }
+ foreach ($sharedCalendars as $id) {
+ $calendarExpressions[] = $query->expr()->andX(
+ $query->expr()->eq('c.calendarid',
+ $query->createNamedParameter($id)),
+ $query->expr()->eq('c.classification',
+ $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
+ $query->expr()->eq('c.calendartype',
+ $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
+ }
- // Component expressions
- $compExpressions = [];
- foreach ($filters['comps'] as $comp) {
- $compExpressions[] = $query->expr()
- ->eq('c.componenttype', $query->createNamedParameter($comp));
- }
+ if (count($calendarExpressions) === 1) {
+ $calExpr = $calendarExpressions[0];
+ } else {
+ $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
+ }
- if (count($compExpressions) === 1) {
- $compExpr = $compExpressions[0];
- } else {
- $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
- }
+ // Component expressions
+ $compExpressions = [];
+ foreach ($filters['comps'] as $comp) {
+ $compExpressions[] = $query->expr()
+ ->eq('c.componenttype', $query->createNamedParameter($comp));
+ }
- if (!isset($filters['props'])) {
- $filters['props'] = [];
- }
- if (!isset($filters['params'])) {
- $filters['params'] = [];
- }
+ if (count($compExpressions) === 1) {
+ $compExpr = $compExpressions[0];
+ } else {
+ $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
+ }
- $propParamExpressions = [];
- foreach ($filters['props'] as $prop) {
- $propParamExpressions[] = $query->expr()->andX(
- $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
- $query->expr()->isNull('i.parameter')
- );
- }
- foreach ($filters['params'] as $param) {
- $propParamExpressions[] = $query->expr()->andX(
- $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
- $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
- );
- }
+ if (!isset($filters['props'])) {
+ $filters['props'] = [];
+ }
+ if (!isset($filters['params'])) {
+ $filters['params'] = [];
+ }
- if (count($propParamExpressions) === 1) {
- $propParamExpr = $propParamExpressions[0];
- } else {
- $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
- }
+ $propParamExpressions = [];
+ foreach ($filters['props'] as $prop) {
+ $propParamExpressions[] = $query->expr()->andX(
+ $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
+ $query->expr()->isNull('i.parameter')
+ );
+ }
+ foreach ($filters['params'] as $param) {
+ $propParamExpressions[] = $query->expr()->andX(
+ $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
+ $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
+ );
+ }
- $query->select(['c.calendarid', 'c.uri'])
- ->from($this->dbObjectPropertiesTable, 'i')
- ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
- ->where($calExpr)
- ->andWhere($compExpr)
- ->andWhere($propParamExpr)
- ->andWhere($query->expr()->iLike('i.value',
- $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%')))
- ->andWhere($query->expr()->isNull('deleted_at'));
+ if (count($propParamExpressions) === 1) {
+ $propParamExpr = $propParamExpressions[0];
+ } else {
+ $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
+ }
- if ($offset) {
- $query->setFirstResult($offset);
- }
- if ($limit) {
- $query->setMaxResults($limit);
- }
+ $query->select(['c.calendarid', 'c.uri'])
+ ->from($this->dbObjectPropertiesTable, 'i')
+ ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
+ ->where($calExpr)
+ ->andWhere($compExpr)
+ ->andWhere($propParamExpr)
+ ->andWhere($query->expr()->iLike('i.value',
+ $query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%')))
+ ->andWhere($query->expr()->isNull('deleted_at'));
+
+ if ($offset) {
+ $query->setFirstResult($offset);
+ }
+ if ($limit) {
+ $query->setMaxResults($limit);
+ }
- $stmt = $query->executeQuery();
+ $stmt = $query->executeQuery();
- $result = [];
- while ($row = $stmt->fetch()) {
- $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
- if (!in_array($path, $result)) {
- $result[] = $path;
+ $result = [];
+ while ($row = $stmt->fetch()) {
+ $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
+ if (!in_array($path, $result)) {
+ $result[] = $path;
+ }
}
- }
- return $result;
+ return $result;
+ }, $this->db);
}
/**
@@ -1876,110 +1965,151 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
*
* @return array
*/
- public function search(array $calendarInfo, $pattern, array $searchProperties,
- array $options, $limit, $offset) {
+ public function search(
+ array $calendarInfo,
+ $pattern,
+ array $searchProperties,
+ array $options,
+ $limit,
+ $offset,
+ ) {
$outerQuery = $this->db->getQueryBuilder();
$innerQuery = $this->db->getQueryBuilder();
+ if (isset($calendarInfo['source'])) {
+ $calendarType = self::CALENDAR_TYPE_SUBSCRIPTION;
+ } else {
+ $calendarType = self::CALENDAR_TYPE_CALENDAR;
+ }
+
$innerQuery->selectDistinct('op.objectid')
->from($this->dbObjectPropertiesTable, 'op')
->andWhere($innerQuery->expr()->eq('op.calendarid',
$outerQuery->createNamedParameter($calendarInfo['id'])))
->andWhere($innerQuery->expr()->eq('op.calendartype',
- $outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
+ $outerQuery->createNamedParameter($calendarType)));
+
+ $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
+ ->from('calendarobjects', 'c')
+ ->where($outerQuery->expr()->isNull('deleted_at'));
// only return public items for shared calendars for now
if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
- $innerQuery->andWhere($innerQuery->expr()->eq('c.classification',
+ $outerQuery->andWhere($outerQuery->expr()->eq('c.classification',
$outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
}
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) . '%')));
}
- $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
- ->from('calendarobjects', 'c')
- ->where($outerQuery->expr()->isNull('deleted_at'));
+ $start = null;
+ $end = null;
- if (isset($options['timerange'])) {
- if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
- $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
- $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
- }
- if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
- $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
- $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
- }
+ $hasLimit = is_int($limit);
+ $hasTimeRange = false;
+
+ if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
+ /** @var DateTimeInterface $start */
+ $start = $options['timerange']['start'];
+ $outerQuery->andWhere(
+ $outerQuery->expr()->gt(
+ 'lastoccurence',
+ $outerQuery->createNamedParameter($start->getTimestamp())
+ )
+ );
+ $hasTimeRange = true;
+ }
+
+ if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
+ /** @var DateTimeInterface $end */
+ $end = $options['timerange']['end'];
+ $outerQuery->andWhere(
+ $outerQuery->expr()->lt(
+ 'firstoccurence',
+ $outerQuery->createNamedParameter($end->getTimestamp())
+ )
+ );
+ $hasTimeRange = true;
+ }
+
+ if (isset($options['uid'])) {
+ $outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
}
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())));
- if ($offset) {
- $outerQuery->setFirstResult($offset);
- }
- if ($limit) {
+ // Without explicit order by its undefined in which order the SQL server returns the events.
+ // For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
+ $outerQuery->addOrderBy('id');
+
+ $offset = (int)$offset;
+ $outerQuery->setFirstResult($offset);
+
+ $calendarObjects = [];
+
+ if ($hasLimit && $hasTimeRange) {
+ /**
+ * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
+ *
+ * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
+ * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
+ *
+ * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
+ * and discard the events after evaluating the reoccurrence rules because they are not due within
+ * the next 14 days and end up with an empty result even if there are two events to show.
+ *
+ * The workaround for search requests with a limit and time range is asking for more row than requested
+ * and retrying if we have not reached the limit.
+ *
+ * 25 rows and 3 retries is entirely arbitrary.
+ */
+ $maxResults = (int)max($limit, 25);
+ $outerQuery->setMaxResults($maxResults);
+
+ for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) {
+ $objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjects($outerQuery, $start, $end));
+ $outerQuery->setFirstResult($offset += $maxResults);
+ }
+
+ $calendarObjects = array_slice($calendarObjects, 0, $limit, false);
+ } else {
$outerQuery->setMaxResults($limit);
+ $calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
}
- $result = $outerQuery->executeQuery();
- $calendarObjects = array_filter($result->fetchAll(), function (array $row) use ($options) {
- $start = $options['timerange']['start'] ?? null;
- $end = $options['timerange']['end'] ?? null;
+ $calendarObjects = array_map(function ($o) use ($options) {
+ $calendarData = Reader::read($o['calendardata']);
- if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
- // No filter required
- return true;
+ // Expand recurrences if an explicit time range is requested
+ if ($calendarData instanceof VCalendar
+ && isset($options['timerange']['start'], $options['timerange']['end'])) {
+ $calendarData = $calendarData->expand(
+ $options['timerange']['start'],
+ $options['timerange']['end'],
+ );
}
- $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,
- ]);
- if (is_resource($row['calendardata'])) {
- // Put the stream back to the beginning so it can be read another time
- rewind($row['calendardata']);
- }
- return $isValid;
- });
- $result->closeCursor();
-
- return array_map(function ($o) {
- $calendarData = Reader::read($o['calendardata']);
$comps = $calendarData->getComponents();
$objects = [];
$timezones = [];
@@ -2004,6 +2134,72 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}, $timezones),
];
}, $calendarObjects);
+
+ usort($calendarObjects, function (array $a, array $b) {
+ /** @var DateTimeImmutable $startA */
+ $startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
+ /** @var DateTimeImmutable $startB */
+ $startB = $b['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
+
+ return $startA->getTimestamp() <=> $startB->getTimestamp();
+ });
+
+ return $calendarObjects;
+ }
+
+ private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface $start, ?DateTimeInterface $end): array {
+ $calendarObjects = [];
+ $filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
+
+ $result = $query->executeQuery();
+
+ while (($row = $result->fetch()) !== false) {
+ if ($filterByTimeRange === false) {
+ // No filter required
+ $calendarObjects[] = $row;
+ continue;
+ }
+
+ 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,
+ ]);
+ } 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
+ rewind($row['calendardata']);
+ }
+
+ if ($isValid) {
+ $calendarObjects[] = $row;
+ }
+ }
+
+ $result->closeCursor();
+
+ return $calendarObjects;
}
/**
@@ -2077,115 +2273,136 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return array
*/
public function searchPrincipalUri(string $principalUri,
- string $pattern,
- array $componentTypes,
- array $searchProperties,
- array $searchParameters,
- array $options = []): array {
- $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();
+ string $pattern,
+ array $componentTypes,
+ array $searchProperties,
+ array $searchParameters,
+ 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 = [];
+ $searchOr = [];
+
+ // Fetch calendars and subscription
+ $calendars = $this->getCalendarsForUser($principalUri);
+ $subscriptions = $this->getSubscriptionsForUser($principalUri);
+ foreach ($calendars as $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)),
+ );
- // 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)));
+ // If it's shared, limit search to public events
+ if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
+ && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
+ $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
+ }
- // If it's shared, limit search to public events
- if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
- && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
- $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
+ $calendarOr[] = $calendarAnd;
}
+ foreach ($subscriptions as $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)),
+ );
- $calendarOr->add($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)));
+ // If it's shared, limit search to public events
+ if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
+ && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
+ $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
+ }
- // If it's shared, limit search to public events
- if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
- && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
- $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
+ $calendarOr[] = $subscriptionAnd;
}
- $calendarOr->add($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'));
-
- $searchOr->add($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)));
-
- $searchOr->add($parameterAnd);
- }
+ foreach ($searchProperties as $property) {
+ $propertyAnd = $calendarObjectIdQuery->expr()->andX(
+ $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
+ $calendarObjectIdQuery->expr()->isNull('cob.parameter'),
+ );
- if ($calendarOr->count() === 0) {
- return [];
- }
- if ($searchOr->count() === 0) {
- return [];
- }
+ $searchOr[] = $propertyAnd;
+ }
+ foreach ($searchParameters as $property => $parameter) {
+ $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)),
+ );
- $calendarObjectIdQuery->selectDistinct('cob.objectid')
- ->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()->isNull('deleted_at'));
+ $searchOr[] = $parameterAnd;
+ }
- if ('' !== $pattern) {
- if (!$escapePattern) {
- $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
- } else {
- $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
+ if (empty($calendarOr)) {
+ return [];
+ }
+ if (empty($searchOr)) {
+ return [];
}
- }
- if (isset($options['limit'])) {
- $calendarObjectIdQuery->setMaxResults($options['limit']);
- }
- if (isset($options['offset'])) {
- $calendarObjectIdQuery->setFirstResult($options['offset']);
- }
+ $calendarObjectIdQuery->selectDistinct('cob.objectid')
+ ->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($calendarObjectIdQuery->expr()->orX(...$calendarOr))
+ ->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr))
+ ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
+
+ if ($pattern !== '') {
+ if (!$escapePattern) {
+ $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
+ } else {
+ $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
+ }
+ }
- $result = $calendarObjectIdQuery->executeQuery();
- $matches = $result->fetchAll();
- $result->closeCursor();
- $matches = array_map(static function (array $match):int {
- return (int) $match['objectid'];
- }, $matches);
+ if (isset($options['limit'])) {
+ $calendarObjectIdQuery->setMaxResults($options['limit']);
+ }
+ if (isset($options['offset'])) {
+ $calendarObjectIdQuery->setFirstResult($options['offset']);
+ }
+ if (isset($options['timerange'])) {
+ if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
+ $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
+ 'lastoccurence',
+ $calendarObjectIdQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()),
+ ));
+ }
+ if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
+ $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
+ 'firstoccurence',
+ $calendarObjectIdQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()),
+ ));
+ }
+ }
- $query = $this->db->getQueryBuilder();
- $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
- ->from('calendarobjects')
- ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
+ $result = $calendarObjectIdQuery->executeQuery();
+ $matches = [];
+ while (($row = $result->fetch()) !== false) {
+ $matches[] = (int)$row['objectid'];
+ }
+ $result->closeCursor();
- $result = $query->executeQuery();
- $calendarObjects = $result->fetchAll();
- $result->closeCursor();
+ $query = $this->db->getQueryBuilder();
+ $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
+ ->from('calendarobjects')
+ ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
- return array_map(function (array $array): array {
- $array['calendarid'] = (int)$array['calendarid'];
- $array['calendartype'] = (int)$array['calendartype'];
- $array['calendardata'] = $this->readBlob($array['calendardata']);
+ $result = $query->executeQuery();
+ $calendarObjects = [];
+ while (($array = $result->fetch()) !== false) {
+ $array['calendarid'] = (int)$array['calendarid'];
+ $array['calendartype'] = (int)$array['calendartype'];
+ $array['calendardata'] = $this->readBlob($array['calendardata']);
- return $array;
- }, $calendarObjects);
+ $calendarObjects[] = $array;
+ }
+ $result->closeCursor();
+ return $calendarObjects;
+ }, $this->db);
}
/**
@@ -2252,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,
];
}
@@ -2311,87 +2528,74 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param int $syncLevel
* @param int|null $limit
* @param int $calendarType
- * @return array
+ * @return ?array
*/
public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
- // Current synctoken
- $qb = $this->db->getQueryBuilder();
- $qb->select('synctoken')
- ->from('calendars')
- ->where(
- $qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
- );
- $stmt = $qb->executeQuery();
- $currentToken = $stmt->fetchOne();
-
- if ($currentToken === false) {
- return null;
- }
-
- $result = [
- 'syncToken' => $currentToken,
- 'added' => [],
- 'modified' => [],
- 'deleted' => [],
- ];
+ $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
- if ($syncToken) {
+ return $this->atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType, $table) {
+ // Current synctoken
$qb = $this->db->getQueryBuilder();
-
- $qb->select('uri', 'operation')
- ->from('calendarchanges')
+ $qb->select('synctoken')
+ ->from($table)
->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
+ $qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
+ );
$stmt = $qb->executeQuery();
- $changes = [];
+ $currentToken = $stmt->fetchOne();
+ $initialSync = !is_numeric($syncToken);
- // 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'];
+ if ($currentToken === false) {
+ return null;
}
- $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;
- }
+ // 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()->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');
}
- } else {
- // No synctoken supplied, this is the initial sync.
- $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))
- )
- );
+ // evaluate if limit exists
+ if (is_numeric($limit)) {
+ $qb->setMaxResults($limit);
+ }
+ // execute command
$stmt = $qb->executeQuery();
- $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ // build results
+ $result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []];
+ // retrieve results
+ if ($initialSync) {
+ $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+ } 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;
+
+ return $result;
+ }, $this->db);
}
/**
@@ -2452,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);
@@ -2495,28 +2699,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
- $valuesToInsert = [];
-
- $query = $this->db->getQueryBuilder();
-
- foreach (array_keys($values) as $name) {
- $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
- }
+ [$subscriptionId, $subscriptionRow] = $this->atomic(function () use ($values) {
+ $valuesToInsert = [];
+ $query = $this->db->getQueryBuilder();
+ foreach (array_keys($values) as $name) {
+ $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
+ }
+ $query->insert('calendarsubscriptions')
+ ->values($valuesToInsert)
+ ->executeStatement();
- $query->insert('calendarsubscriptions')
- ->values($valuesToInsert)
- ->executeStatement();
+ $subscriptionId = $query->getLastInsertId();
- $subscriptionId = $query->getLastInsertId();
+ $subscriptionRow = $this->getSubscriptionById($subscriptionId);
+ return [$subscriptionId, $subscriptionRow];
+ }, $this->db);
- $subscriptionRow = $this->getSubscriptionById($subscriptionId);
$this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::createSubscription',
- [
- 'subscriptionId' => $subscriptionId,
- 'subscriptionData' => $subscriptionRow,
- ]));
return $subscriptionId;
}
@@ -2553,24 +2752,20 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
}
- $query = $this->db->getQueryBuilder();
- $query->update('calendarsubscriptions')
- ->set('lastmodified', $query->createNamedParameter(time()));
- foreach ($newValues as $fieldName => $value) {
- $query->set($fieldName, $query->createNamedParameter($value));
- }
- $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
- ->executeStatement();
+ $subscriptionRow = $this->atomic(function () use ($subscriptionId, $newValues) {
+ $query = $this->db->getQueryBuilder();
+ $query->update('calendarsubscriptions')
+ ->set('lastmodified', $query->createNamedParameter(time()));
+ foreach ($newValues as $fieldName => $value) {
+ $query->set($fieldName, $query->createNamedParameter($value));
+ }
+ $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
+ ->executeStatement();
+
+ return $this->getSubscriptionById($subscriptionId);
+ }, $this->db);
- $subscriptionRow = $this->getSubscriptionById($subscriptionId);
$this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription',
- [
- 'subscriptionId' => $subscriptionId,
- 'subscriptionData' => $subscriptionRow,
- 'propertyMutations' => $mutations,
- ]));
return true;
});
@@ -2583,39 +2778,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteSubscription($subscriptionId) {
- $subscriptionRow = $this->getSubscriptionById($subscriptionId);
-
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription',
- [
- 'subscriptionId' => $subscriptionId,
- 'subscriptionData' => $this->getSubscriptionById($subscriptionId),
- ]));
+ $this->atomic(function () use ($subscriptionId): void {
+ $subscriptionRow = $this->getSubscriptionById($subscriptionId);
- $query = $this->db->getQueryBuilder();
- $query->delete('calendarsubscriptions')
- ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
- ->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarsubscriptions')
+ ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
+ ->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)))
- ->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)))
+ ->executeStatement();
- $query->delete('calendarchanges')
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
- ->executeStatement();
+ $query->delete('calendarchanges')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->executeStatement();
- $query->delete($this->dbObjectPropertiesTable)
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
- ->executeStatement();
+ $query->delete($this->dbObjectPropertiesTable)
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
+ ->executeStatement();
- if ($subscriptionRow) {
- $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
- }
+ if ($subscriptionRow) {
+ $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
+ }
+ }, $this->db);
}
/**
@@ -2671,13 +2861,13 @@ 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();
- $result = [];
- foreach ($stmt->fetchAll() as $row) {
- $result[] = [
+ $results = [];
+ while (($row = $stmt->fetch()) !== false) {
+ $results[] = [
'calendardata' => $row['calendardata'],
'uri' => $row['uri'],
'lastmodified' => $row['lastmodified'],
@@ -2687,7 +2877,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
$stmt->closeCursor();
- return $result;
+ return $results;
}
/**
@@ -2698,11 +2888,50 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function deleteSchedulingObject($principalUri, $objectUri) {
+ $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();
+ }
+
+ /**
+ * Deletes all scheduling objects last modified before $modifiedBefore from the inbox collection.
+ *
+ * @param int $modifiedBefore
+ * @param int $limit
+ * @return void
+ */
+ public function deleteOutdatedSchedulingObjects(int $modifiedBefore, int $limit): void {
+ $query = $this->db->getQueryBuilder();
+ $query->select('id')
+ ->from('schedulingobjects')
+ ->where($query->expr()->lt('lastmodified', $query->createNamedParameter($modifiedBefore)))
+ ->setMaxResults($limit);
+ $result = $query->executeQuery();
+ $count = $result->rowCount();
+ if ($count === 0) {
+ return;
+ }
+ $ids = array_map(static function (array $id) {
+ return (int)$id[0];
+ }, $result->fetchAll(\PDO::FETCH_NUM));
+ $result->closeCursor();
+
+ $numDeleted = 0;
+ $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) {
+ $deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $numDeleted += $deleteQuery->executeStatement();
+ }
+
+ if ($numDeleted === $limit) {
+ $this->logger->info("Deleted $limit scheduling objects, continuing with next batch");
+ $this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit);
+ }
}
/**
@@ -2714,6 +2943,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @return void
*/
public function createSchedulingObject($principalUri, $objectUri, $objectData) {
+ $this->cachedObjects = [];
$query = $this->db->getQueryBuilder();
$query->insert('schedulingobjects')
->values([
@@ -2731,37 +2961,86 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* Adds a change record to the calendarchanges table.
*
* @param mixed $calendarId
- * @param string $objectUri
+ * @param string[] $objectUris
* @param int $operation 1 = add, 2 = modify, 3 = delete.
* @param int $calendarType
* @return void
*/
- protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
+ protected function addChanges(int $calendarId, array $objectUris, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
+ $this->cachedObjects = [];
$table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
- $query = $this->db->getQueryBuilder();
- $query->select('synctoken')
- ->from($table)
- ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
- $result = $query->executeQuery();
- $syncToken = (int)$result->fetchOne();
- $result->closeCursor();
+ $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
+ $query = $this->db->getQueryBuilder();
+ $query->select('synctoken')
+ ->from($table)
+ ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
+ $result = $query->executeQuery();
+ $syncToken = (int)$result->fetchOne();
+ $result->closeCursor();
- $query = $this->db->getQueryBuilder();
- $query->insert('calendarchanges')
- ->values([
- 'uri' => $query->createNamedParameter($objectUri),
- 'synctoken' => $query->createNamedParameter($syncToken),
- 'calendarid' => $query->createNamedParameter($calendarId),
- 'operation' => $query->createNamedParameter($operation),
- 'calendartype' => $query->createNamedParameter($calendarType),
- ])
- ->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendarchanges')
+ ->values([
+ 'uri' => $query->createParameter('uri'),
+ 'synctoken' => $query->createNamedParameter($syncToken),
+ 'calendarid' => $query->createNamedParameter($calendarId),
+ 'operation' => $query->createNamedParameter($operation),
+ 'calendartype' => $query->createNamedParameter($calendarType),
+ 'created_at' => $query->createNamedParameter(time()),
+ ]);
+ foreach ($objectUris as $uri) {
+ $query->setParameter('uri', $uri);
+ $query->executeStatement();
+ }
- $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?");
- $stmt->execute([
- $calendarId
- ]);
+ $query = $this->db->getQueryBuilder();
+ $query->update($table)
+ ->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT))
+ ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
+ ->executeStatement();
+ }, $this->db);
+ }
+
+ public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
+ $this->cachedObjects = [];
+
+ $this->atomic(function () use ($calendarId, $calendarType): void {
+ $qbAdded = $this->db->getQueryBuilder();
+ $qbAdded->select('uri')
+ ->from('calendarobjects')
+ ->where(
+ $qbAdded->expr()->andX(
+ $qbAdded->expr()->eq('calendarid', $qbAdded->createNamedParameter($calendarId)),
+ $qbAdded->expr()->eq('calendartype', $qbAdded->createNamedParameter($calendarType)),
+ $qbAdded->expr()->isNull('deleted_at'),
+ )
+ );
+ $resultAdded = $qbAdded->executeQuery();
+ $addedUris = $resultAdded->fetchAll(\PDO::FETCH_COLUMN);
+ $resultAdded->closeCursor();
+ // Track everything as changed
+ // Tracking the creation is not necessary because \OCA\DAV\CalDAV\CalDavBackend::getChangesForCalendar
+ // only returns the last change per object.
+ $this->addChanges($calendarId, $addedUris, 2, $calendarType);
+
+ $qbDeleted = $this->db->getQueryBuilder();
+ $qbDeleted->select('uri')
+ ->from('calendarobjects')
+ ->where(
+ $qbDeleted->expr()->andX(
+ $qbDeleted->expr()->eq('calendarid', $qbDeleted->createNamedParameter($calendarId)),
+ $qbDeleted->expr()->eq('calendartype', $qbDeleted->createNamedParameter($calendarType)),
+ $qbDeleted->expr()->isNotNull('deleted_at'),
+ )
+ );
+ $resultDeleted = $qbDeleted->executeQuery();
+ $deletedUris = array_map(function (string $uri) {
+ return str_replace('-deleted.ics', '.ics', $uri);
+ }, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
+ $resultDeleted->closeCursor();
+ $this->addChanges($calendarId, $deletedUris, 3, $calendarType);
+ }, $this->db);
}
/**
@@ -2779,7 +3058,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param string $calendarData
* @return array
*/
- public function getDenormalizedData($calendarData) {
+ public function getDenormalizedData(string $calendarData): array {
$vObject = Reader::read($calendarData);
$vEvents = [];
$componentType = null;
@@ -2793,7 +3072,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
if ($component->name !== 'VTIMEZONE') {
// Finding all VEVENTs, and track them
if ($component->name === 'VEVENT') {
- array_push($vEvents, $component);
+ $vEvents[] = $component;
if ($component->DTSTART) {
$hasDTSTART = true;
}
@@ -2829,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()) {
@@ -2861,7 +3148,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
'size' => strlen($calendarData),
'componentType' => $componentType,
'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
- 'lastOccurence' => $lastOccurrence,
+ 'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
'uid' => $uid,
'classification' => $classification
];
@@ -2880,80 +3167,73 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
- * @param IShareable $shareable
- * @param array $add
- * @param array $remove
+ * @param list<array{href: string, commonName: string, readOnly: bool}> $add
+ * @param list<string> $remove
*/
- public function updateShares($shareable, $add, $remove) {
- $calendarId = $shareable->getResourceId();
- $calendarRow = $this->getCalendarById($calendarId);
- $oldShares = $this->getShares($calendarId);
+ public function updateShares(IShareable $shareable, array $add, array $remove): void {
+ $this->atomic(function () use ($shareable, $add, $remove): void {
+ $calendarId = $shareable->getResourceId();
+ $calendarRow = $this->getCalendarById($calendarId);
+ if ($calendarRow === null) {
+ throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $calendarId);
+ }
+ $oldShares = $this->getShares($calendarId);
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
- [
- 'calendarId' => $calendarId,
- 'calendarData' => $calendarRow,
- 'shares' => $oldShares,
- 'add' => $add,
- 'remove' => $remove,
- ]));
- $this->calendarSharingBackend->updateShares($shareable, $add, $remove);
+ $this->calendarSharingBackend->updateShares($shareable, $add, $remove, $oldShares);
- $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove));
+ $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove));
+ }, $this->db);
}
/**
- * @param int $resourceId
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares($resourceId) {
+ public function getShares(int $resourceId): array {
return $this->calendarSharingBackend->getShares($resourceId);
}
+ public function preloadShares(array $resourceIds): void {
+ $this->calendarSharingBackend->preloadShares($resourceIds);
+ }
+
/**
* @param boolean $value
- * @param \OCA\DAV\CalDAV\Calendar $calendar
+ * @param Calendar $calendar
* @return string|null
*/
public function setPublishStatus($value, $calendar) {
- $calendarId = $calendar->getResourceId();
- $calendarData = $this->getCalendarById($calendarId);
- $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent(
- '\OCA\DAV\CalDAV\CalDavBackend::updateShares',
- [
- 'calendarId' => $calendarId,
- 'calendarData' => $calendarData,
- 'public' => $value,
- ]));
+ return $this->atomic(function () use ($value, $calendar) {
+ $calendarId = $calendar->getResourceId();
+ $calendarData = $this->getCalendarById($calendarId);
- $query = $this->db->getQueryBuilder();
- if ($value) {
- $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
- $query->insert('dav_shares')
- ->values([
- 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
- 'type' => $query->createNamedParameter('calendar'),
- 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
- 'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
- 'publicuri' => $query->createNamedParameter($publicUri)
- ]);
- $query->executeStatement();
+ $query = $this->db->getQueryBuilder();
+ if ($value) {
+ $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
+ $query->insert('dav_shares')
+ ->values([
+ 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
+ 'type' => $query->createNamedParameter('calendar'),
+ 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
+ 'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
+ 'publicuri' => $query->createNamedParameter($publicUri)
+ ]);
+ $query->executeStatement();
- $this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri));
- return $publicUri;
- }
- $query->delete('dav_shares')
- ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
- ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
- $query->executeStatement();
+ $this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri));
+ return $publicUri;
+ }
+ $query->delete('dav_shares')
+ ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
+ ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
+ $query->executeStatement();
- $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData));
- return null;
+ $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData));
+ return null;
+ }, $this->db);
}
/**
- * @param \OCA\DAV\CalDAV\Calendar $calendar
+ * @param Calendar $calendar
* @return mixed
*/
public function getPublishStatus($calendar) {
@@ -2971,15 +3251,14 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
/**
* @param int $resourceId
- * @param array $acl
- * @return array
+ * @param list<array{privilege: string, principal: string, protected: bool}> $acl
+ * @return list<array{privilege: string, principal: string, protected: bool}>
*/
- public function applyShareAcl($resourceId, $acl) {
- return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl);
+ public function applyShareAcl(int $resourceId, array $acl): array {
+ $shares = $this->calendarSharingBackend->getShares($resourceId);
+ return $this->calendarSharingBackend->applyShareAcl($shares, $acl);
}
-
-
/**
* update properties table
*
@@ -2989,127 +3268,172 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param int $calendarType
*/
public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
- $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
+ $this->cachedObjects = [];
+ $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void {
+ $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
+
+ try {
+ $vCalendar = $this->readCalendarData($calendarData);
+ } catch (\Exception $ex) {
+ return;
+ }
- try {
- $vCalendar = $this->readCalendarData($calendarData);
- } catch (\Exception $ex) {
- return;
- }
+ $this->purgeProperties($calendarId, $objectId);
- $this->purgeProperties($calendarId, $objectId);
+ $query = $this->db->getQueryBuilder();
+ $query->insert($this->dbObjectPropertiesTable)
+ ->values(
+ [
+ 'calendarid' => $query->createNamedParameter($calendarId),
+ 'calendartype' => $query->createNamedParameter($calendarType),
+ 'objectid' => $query->createNamedParameter($objectId),
+ 'name' => $query->createParameter('name'),
+ 'parameter' => $query->createParameter('parameter'),
+ 'value' => $query->createParameter('value'),
+ ]
+ );
- $query = $this->db->getQueryBuilder();
- $query->insert($this->dbObjectPropertiesTable)
- ->values(
- [
- 'calendarid' => $query->createNamedParameter($calendarId),
- 'calendartype' => $query->createNamedParameter($calendarType),
- 'objectid' => $query->createNamedParameter($objectId),
- 'name' => $query->createParameter('name'),
- 'parameter' => $query->createParameter('parameter'),
- 'value' => $query->createParameter('value'),
- ]
- );
+ $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
+ foreach ($vCalendar->getComponents() as $component) {
+ if (!in_array($component->name, $indexComponents)) {
+ continue;
+ }
- $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
- foreach ($vCalendar->getComponents() as $component) {
- if (!in_array($component->name, $indexComponents)) {
- continue;
- }
+ foreach ($component->children() as $property) {
+ if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
+ $value = $property->getValue();
+ // is this a shitty db?
+ if (!$this->db->supports4ByteText()) {
+ $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
+ }
+ $value = mb_strcut($value, 0, 254);
- foreach ($component->children() as $property) {
- if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
- $value = $property->getValue();
- // is this a shitty db?
- if (!$this->db->supports4ByteText()) {
- $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
+ $query->setParameter('name', $property->name);
+ $query->setParameter('parameter', null);
+ $query->setParameter('value', mb_strcut($value, 0, 254));
+ $query->executeStatement();
}
- $value = mb_strcut($value, 0, 254);
-
- $query->setParameter('name', $property->name);
- $query->setParameter('parameter', null);
- $query->setParameter('value', $value);
- $query->executeStatement();
- }
-
- if (array_key_exists($property->name, self::$indexParameters)) {
- $parameters = $property->parameters();
- $indexedParametersForProperty = self::$indexParameters[$property->name];
- foreach ($parameters as $key => $value) {
- if (in_array($key, $indexedParametersForProperty)) {
- // is this a shitty db?
- if ($this->db->supports4ByteText()) {
- $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
+ if (array_key_exists($property->name, self::$indexParameters)) {
+ $parameters = $property->parameters();
+ $indexedParametersForProperty = self::$indexParameters[$property->name];
+
+ foreach ($parameters as $key => $value) {
+ if (in_array($key, $indexedParametersForProperty)) {
+ // is this a shitty db?
+ if ($this->db->supports4ByteText()) {
+ $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
+ }
+
+ $query->setParameter('name', $property->name);
+ $query->setParameter('parameter', mb_strcut($key, 0, 254));
+ $query->setParameter('value', mb_strcut($value, 0, 254));
+ $query->executeStatement();
}
-
- $query->setParameter('name', $property->name);
- $query->setParameter('parameter', mb_strcut($key, 0, 254));
- $query->setParameter('value', mb_strcut($value, 0, 254));
- $query->executeStatement();
}
}
}
}
- }
+ }, $this->db);
}
/**
* deletes all birthday calendars
*/
public function deleteAllBirthdayCalendars() {
- $query = $this->db->getQueryBuilder();
- $result = $query->select(['id'])->from('calendars')
- ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
- ->executeQuery();
+ $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)))
+ ->executeQuery();
- $ids = $result->fetchAll();
- $result->closeCursor();
- foreach ($ids as $id) {
- $this->deleteCalendar(
- $id['id'],
- true // No data to keep in the trashbin, if the user re-enables then we regenerate
- );
- }
+ while (($row = $result->fetch()) !== false) {
+ $this->deleteCalendar(
+ $row['id'],
+ true // No data to keep in the trashbin, if the user re-enables then we regenerate
+ );
+ }
+ $result->closeCursor();
+ }, $this->db);
}
/**
* @param $subscriptionId
*/
public function purgeAllCachedEventsForSubscription($subscriptionId) {
- $query = $this->db->getQueryBuilder();
- $query->select('uri')
- ->from('calendarobjects')
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
- $stmt = $query->executeQuery();
+ $this->atomic(function () use ($subscriptionId): void {
+ $query = $this->db->getQueryBuilder();
+ $query->select('uri')
+ ->from('calendarobjects')
+ ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
+ ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
+ $stmt = $query->executeQuery();
- $uris = [];
- foreach ($stmt->fetchAll() as $row) {
- $uris[] = $row['uri'];
- }
- $stmt->closeCursor();
+ $uris = [];
+ while (($row = $stmt->fetch()) !== false) {
+ $uris[] = $row['uri'];
+ }
+ $stmt->closeCursor();
- $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)))
- ->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)))
+ ->executeStatement();
- $query->delete('calendarchanges')
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
- ->executeStatement();
+ $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)))
+ ->executeStatement();
- $query->delete($this->dbObjectPropertiesTable)
- ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
- ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
- ->executeStatement();
+ $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)))
+ ->executeStatement();
+
+ $this->addChanges($subscriptionId, $uris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
+ }, $this->db);
+ }
- foreach ($uris as $uri) {
- $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_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);
}
/**
@@ -3147,6 +3471,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
* @param int $objectId
*/
protected function purgeProperties($calendarId, $objectId) {
+ $this->cachedObjects = [];
$query = $this->db->getQueryBuilder();
$query->delete($this->dbObjectPropertiesTable)
->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
@@ -3182,6 +3507,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * @throws \InvalidArgumentException
+ */
+ public function pruneOutdatedSyncTokens(int $keep, int $retention): int {
+ if ($keep < 0) {
+ throw new \InvalidArgumentException();
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->select($query->func()->max('id'))
+ ->from('calendarchanges');
+
+ $result = $query->executeQuery();
+ $maxId = (int)$result->fetchOne();
+ $result->closeCursor();
+ if (!$maxId || $maxId < $keep) {
+ return 0;
+ }
+
+ $query = $this->db->getQueryBuilder();
+ $query->delete('calendarchanges')
+ ->where(
+ $query->expr()->lte('id', $query->createNamedParameter($maxId - $keep, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
+ $query->expr()->lte('created_at', $query->createNamedParameter($retention)),
+ );
+ return $query->executeStatement();
+ }
+
+ /**
* return legacy endpoint principal name to new principal name
*
* @param $principalUri
@@ -3270,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 75c815c3b0a..deb00caa93d 100644
--- a/apps/dav/lib/CalDAV/Calendar.php
+++ b/apps/dav/lib/CalDAV/Calendar.php
@@ -1,30 +1,10 @@
<?php
+
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Gary Kim <gary@garykim.dev>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -51,28 +31,16 @@ use Sabre\DAV\PropPatch;
* @property CalDavBackend $caldavBackend
*/
class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget {
-
- /** @var IConfig */
- private $config;
-
- /** @var IL10N */
- protected $l10n;
-
- /** @var bool */
- private $useTrashbin = true;
-
- /** @var LoggerInterface */
- private $logger;
-
- /**
- * Calendar constructor.
- *
- * @param BackendInterface $caldavBackend
- * @param $calendarInfo
- * @param IL10N $l10n
- * @param IConfig $config
- */
- public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
+ protected IL10N $l10n;
+ private bool $useTrashbin = true;
+
+ public function __construct(
+ BackendInterface $caldavBackend,
+ array $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())
@@ -82,39 +50,25 @@ 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;
+ }
+
+ public function getUri(): string {
+ return $this->calendarInfo['uri'];
}
/**
- * Updates the list of shares.
- *
- * The first array is a list of people that are to be added to the
- * resource.
- *
- * Every element in the add array has the following properties:
- * * href - A url. Usually a mailto: address
- * * commonName - Usually a first and last name, or false
- * * summary - A description of the share, can also be false
- * * readOnly - A boolean value
- *
- * Every element in the remove array is just the address string.
- *
- * @param array $add
- * @param array $remove
- * @return void
+ * {@inheritdoc}
* @throws Forbidden
*/
- public function updateShares(array $add, array $remove) {
+ public function updateShares(array $add, array $remove): void {
if ($this->isShared()) {
throw new Forbidden();
}
@@ -131,19 +85,16 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
* * readOnly - boolean
* * summary - Optional, a description for the share
*
- * @return array
+ * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
- public function getShares() {
+ public function getShares(): array {
if ($this->isShared()) {
return [];
}
return $this->caldavBackend->getShares($this->getResourceId());
}
- /**
- * @return int
- */
- public function getResourceId() {
+ public function getResourceId(): int {
return $this->calendarInfo['id'];
}
@@ -155,7 +106,9 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
}
/**
- * @return array
+ * @param int $resourceId
+ * @param list<array{privilege: string, principal: string, protected: bool}> $acl
+ * @return list<array{privilege: string, principal: ?string, protected: bool}>
*/
public function getACL() {
$acl = [
@@ -241,21 +194,23 @@ 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'
];
- return array_filter($acl, function ($rule) use ($allowedPrincipals) {
+ /** @var list<array{privilege: string, principal: string, protected: bool}> $acl */
+ $acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool {
return \in_array($rule['principal'], $allowedPrincipals, true);
});
+ return $acl;
}
public function getChildACL() {
return $this->getACL();
}
- public function getOwner() {
+ public function getOwner(): ?string {
if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
}
@@ -263,20 +218,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();
- $shares = $this->caldavBackend->getShares($this->getResourceId());
- $shares = array_filter($shares, function ($share) use ($principal) {
- return $share['href'] === $principal;
- });
- if (empty($shares)) {
- throw new Forbidden();
- }
-
- $this->caldavBackend->updateShares($this, [], [
- $principal
- ]);
+ if ($this->isShared()) {
+ $this->caldavBackend->unshare($this, 'principal:' . $this->getPrincipalURI());
return;
}
@@ -400,7 +343,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
return isset($this->calendarInfo['{http://owncloud.org/ns}public']);
}
- protected function isShared() {
+ public function isShared() {
if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return false;
}
@@ -412,6 +355,13 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable
return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']);
}
+ public function isDeleted(): bool {
+ if (!isset($this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
+ return false;
+ }
+ return $this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] !== null;
+ }
+
/**
* @inheritDoc
*/
@@ -427,7 +377,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 {
@@ -441,9 +391,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->getPrincipalUri());
+ 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 ceeba31800e..89b78ba9007 100644
--- a/apps/dav/lib/CalDAV/CalendarHome.php
+++ b/apps/dav/lib/CalDAV/CalendarHome.php
@@ -1,28 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -30,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;
@@ -44,30 +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 bool */
- private $returnCachedSubscriptions = false;
-
- /** @var LoggerInterface */
- private $logger;
-
- public function __construct(BackendInterface $caldavBackend, $principalInfo, LoggerInterface $logger) {
+ private ?array $cachedChildren = null;
+
+ public function __construct(
+ BackendInterface $caldavBackend,
+ array $principalInfo,
+ 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;
}
/**
@@ -97,6 +81,9 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
* @inheritdoc
*/
public function getChildren() {
+ if ($this->cachedChildren) {
+ return $this->cachedChildren;
+ }
$calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
$objects = [];
foreach ($calendars as $calendar) {
@@ -136,6 +123,7 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
}
}
+ $this->cachedChildren = $objects;
return $objects;
}
@@ -159,7 +147,16 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
return new TrashbinHome($this->caldavBackend, $this->principalInfo);
}
- // Calendars
+ // Calendar - this covers all "regular" calendars, but not shared
+ // only check if the method is available
+ if ($this->caldavBackend instanceof CalDavBackend) {
+ $calendar = $this->caldavBackend->getCalendarByUri($this->principalInfo['uri'], $name);
+ if (!empty($calendar)) {
+ return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
+ }
+ }
+
+ // Fallback to cover shared calendars
foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) {
if ($calendar['uri'] === $name) {
return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
@@ -205,9 +202,4 @@ class CalendarHome extends \Sabre\CalDAV\CalendarHome {
$principalUri = $this->principalInfo['uri'];
return $this->caldavBackend->calendarSearch($principalUri, $filters, $limit, $offset);
}
-
-
- public function enableCachedSubscriptionsForThisRequest() {
- $this->returnCachedSubscriptions = true;
- }
}
diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php
index 406389e3a3d..5f912da732e 100644
--- a/apps/dav/lib/CalDAV/CalendarImpl.php
+++ b/apps/dav/lib/CalDAV/CalendarImpl.php
@@ -3,70 +3,48 @@
declare(strict_types=1);
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
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;
+use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\DAV\Exception\Conflict;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\Component\VTimeZone;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Property;
+use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
-class CalendarImpl implements ICreateFromString {
-
- /** @var CalDavBackend */
- private $backend;
-
- /** @var Calendar */
- private $calendar;
-
- /** @var array */
- private $calendarInfo;
-
- /**
- * CalendarImpl constructor.
- *
- * @param Calendar $calendar
- * @param array $calendarInfo
- * @param CalDavBackend $backend
- */
- 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,
+ ) {
}
/**
* @return string defining the technical unique key
* @since 13.0.0
*/
- public function getKey() {
- return $this->calendarInfo['id'];
+ public function getKey(): string {
+ return (string)$this->calendarInfo['id'];
}
/**
@@ -78,45 +56,60 @@ class CalendarImpl implements ICreateFromString {
/**
* In comparison to getKey() this function returns a human readable (maybe translated) name
- * @return null|string
* @since 13.0.0
*/
- public function getDisplayName() {
+ public function getDisplayName(): ?string {
return $this->calendarInfo['{DAV:}displayname'];
}
/**
* Calendar color
- * @return null|string
* @since 13.0.0
*/
- public function getDisplayColor() {
+ public function getDisplayColor(): ?string {
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 integer|null $limit - limit number of search results
- * @param integer|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($pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null) {
+ public function getSchedulingTransparency(): ?ScheduleCalendarTransp {
+ return $this->calendarInfo['{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}schedule-calendar-transp'];
+ }
+
+ public function getSchedulingTimezone(): ?VTimeZone {
+ $tzProp = '{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}calendar-timezone';
+ if (!isset($this->calendarInfo[$tzProp])) {
+ return null;
+ }
+ // This property contains a VCALENDAR with a single VTIMEZONE
+ /** @var string $timezoneProp */
+ $timezoneProp = $this->calendarInfo[$tzProp];
+ /** @var VCalendar $vobj */
+ $vobj = Reader::read($timezoneProp);
+ $components = $vobj->getComponents();
+ if (empty($components)) {
+ return null;
+ }
+ /** @var VTimeZone $vtimezone */
+ $vtimezone = $components[0];
+ return $vtimezone;
+ }
+
+ 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);
}
/**
- * @return integer build up using \OCP\Constants
+ * @return int build up using \OCP\Constants
* @since 13.0.0
*/
- public function getPermissions() {
+ public function getPermissions(): int {
$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;
@@ -135,19 +128,43 @@ class CalendarImpl implements ICreateFromString {
}
/**
- * Create a new calendar event for this calendar
- * by way of an ICS string
- *
- * @param string $name the file name - needs to contan the .ics ending
- * @param string $calendarData a string containing a valid VEVENT ics
- *
- * @throws CalendarException
+ * @since 32.0.0
*/
- public function createFromString(string $name, string $calendarData): void {
- $server = new InvitationResponseServer(false);
+ 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 {
+ return $this->calendar->isDeleted();
+ }
+
+ /**
+ * @since 31.0.0
+ */
+ 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->server->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
@@ -163,18 +180,113 @@ class CalendarImpl implements ICreateFromString {
// Force calendar change URI
/** @var Schedule\Plugin $schedulingPlugin */
- $schedulingPlugin = $server->server->getPlugin('caldav-schedule');
+ $schedulingPlugin = $server->getPlugin('caldav-schedule');
$schedulingPlugin->setPathOfCalendarObjectChange($fullCalendarFilename);
$stream = fopen('php://memory', 'rb+');
fwrite($stream, $calendarData);
rewind($stream);
try {
- $server->server->createFile($fullCalendarFilename, $stream);
+ $server->createFile($fullCalendarFilename, $stream);
} catch (Conflict $e) {
throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e);
} finally {
fclose($stream);
}
}
+
+ 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
+ */
+ public function handleIMipMessage(string $name, string $calendarData): void {
+ $server = $this->getInvitationResponseServer();
+
+ /** @var CustomPrincipalPlugin $plugin */
+ $plugin = $server->getServer()->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
+ $plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
+
+ if (empty($this->calendarInfo['uri'])) {
+ throw new CalendarException('Could not write to calendar as URI parameter is missing');
+ }
+ // Force calendar change URI
+ /** @var Schedule\Plugin $schedulingPlugin */
+ $schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule');
+ // Let sabre handle the rest
+ $iTipMessage = new Message();
+ /** @var VCalendar $vObject */
+ $vObject = Reader::read($calendarData);
+ /** @var VEvent $vEvent */
+ $vEvent = $vObject->{'VEVENT'};
+
+ if ($vObject->{'METHOD'} === null) {
+ throw new CalendarException('No Method provided for scheduling data. Could not process message');
+ }
+
+ if (!isset($vEvent->{'ORGANIZER'}) || !isset($vEvent->{'ATTENDEE'})) {
+ throw new CalendarException('Could not process scheduling data, neccessary data missing from ICAL');
+ }
+ $organizer = $vEvent->{'ORGANIZER'}->getValue();
+ $attendee = $vEvent->{'ATTENDEE'}->getValue();
+
+ $iTipMessage->method = $vObject->{'METHOD'}->getValue();
+ if ($iTipMessage->method === 'REQUEST') {
+ $iTipMessage->sender = $organizer;
+ $iTipMessage->recipient = $attendee;
+ } elseif ($iTipMessage->method === 'REPLY') {
+ if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
+ $iTipMessage->recipient = $organizer;
+ } else {
+ $iTipMessage->recipient = $attendee;
+ }
+ $iTipMessage->sender = $attendee;
+ } elseif ($iTipMessage->method === 'CANCEL') {
+ $iTipMessage->recipient = $attendee;
+ $iTipMessage->sender = $organizer;
+ }
+ $iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
+ $iTipMessage->component = 'VEVENT';
+ $iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
+ $iTipMessage->message = $vObject;
+ $server->server->emit('schedule', [$iTipMessage]);
+ }
+
+ 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 daa96a51392..a2d2f1cda8a 100644
--- a/apps/dav/lib/CalDAV/CalendarManager.php
+++ b/apps/dav/lib/CalDAV/CalendarManager.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -31,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.
*
@@ -50,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 c927254fba3..02178b4236f 100644
--- a/apps/dav/lib/CalDAV/CalendarObject.php
+++ b/apps/dav/lib/CalDAV/CalendarObject.php
@@ -1,28 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017, Georg Ehrke
- * @copyright Copyright (c) 2020, Gary Kim <gary@garykim.dev>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Gary Kim <gary@garykim.dev>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -33,9 +14,6 @@ use Sabre\VObject\Reader;
class CalendarObject extends \Sabre\CalDAV\CalendarObject {
- /** @var IL10N */
- protected $l10n;
-
/**
* CalendarObject constructor.
*
@@ -44,16 +22,17 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
* @param array $calendarInfo
* @param array $objectData
*/
- public function __construct(CalDavBackend $caldavBackend, IL10N $l10n,
- array $calendarInfo,
- array $objectData) {
+ public function __construct(
+ CalDavBackend $caldavBackend,
+ protected IL10N $l10n,
+ array $calendarInfo,
+ array $objectData,
+ ) {
parent::__construct($caldavBackend, $calendarInfo, $objectData);
if ($this->isShared()) {
unset($this->objectData['size']);
}
-
- $this->l10n = $l10n;
}
/**
@@ -82,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() {
@@ -97,28 +76,29 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
* @param Component\VCalendar $vObject
* @return void
*/
- private function createConfidentialObject(Component\VCalendar $vObject) {
+ private function createConfidentialObject(Component\VCalendar $vObject): void {
/** @var Component $vElement */
- $vElement = null;
- if (isset($vObject->VEVENT)) {
- $vElement = $vObject->VEVENT;
- }
- if (isset($vObject->VJOURNAL)) {
- $vElement = $vObject->VJOURNAL;
- }
- if (isset($vObject->VTODO)) {
- $vElement = $vObject->VTODO;
- }
- if (!is_null($vElement)) {
+ $vElements = array_filter($vObject->getComponents(), static function ($vElement) {
+ return $vElement instanceof Component\VEvent || $vElement instanceof Component\VJournal || $vElement instanceof Component\VTodo;
+ });
+
+ foreach ($vElements as $vElement) {
+ if (empty($vElement->select('SUMMARY'))) {
+ $vElement->add('SUMMARY', $this->l10n->t('Busy')); // This is needed to mask "Untitled Event" events
+ }
foreach ($vElement->children() as &$property) {
/** @var Property $property */
switch ($property->name) {
case 'CREATED':
case 'DTSTART':
case 'RRULE':
+ case 'RECURRENCE-ID':
+ case 'RDATE':
case 'DURATION':
case 'DTEND':
case 'CLASS':
+ case 'EXRULE':
+ case 'EXDATE':
case 'UID':
break;
case 'SUMMARY':
@@ -162,4 +142,11 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
public function getPrincipalUri(): string {
return $this->calendarInfo['principaluri'];
}
+
+ public function getOwner(): ?string {
+ if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
+ return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
+ }
+ return parent::getOwner();
+ }
}
diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php
index f29c601db2d..a8b818e59aa 100644
--- a/apps/dav/lib/CalDAV/CalendarProvider.php
+++ b/apps/dav/lib/CalDAV/CalendarProvider.php
@@ -3,28 +3,13 @@
declare(strict_types=1);
/**
- * @copyright 2021 Anna Larch <anna.larch@gmx.net>
- *
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
+use OCA\DAV\Db\Property;
+use OCA\DAV\Db\PropertyMapper;
use OCP\Calendar\ICalendarProvider;
use OCP\IConfig;
use OCP\IL10N;
@@ -32,39 +17,33 @@ 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);
+ });
+ }
+ $additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
$iCalendars = [];
foreach ($calendarInfos as $calendarInfo) {
+ $user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
+ $path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
+
+ $calendarInfo = array_merge($calendarInfo, $additionalProperties[$path] ?? []);
+
$calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$iCalendars[] = new CalendarImpl(
$calendar,
@@ -74,4 +53,41 @@ class CalendarProvider implements ICalendarProvider {
}
return $iCalendars;
}
+
+ /**
+ * @param array{
+ * principaluri: string,
+ * uri: string,
+ * }[] $uris
+ * @return array<string, array<string, string|bool>>
+ */
+ private function getAdditionalPropertiesForCalendars(array $uris): array {
+ $calendars = [];
+ foreach ($uris as $uri) {
+ /** @var string $user */
+ $user = str_replace('principals/users/', '', $uri['principaluri']);
+ if (!array_key_exists($user, $calendars)) {
+ $calendars[$user] = [];
+ }
+ $calendars[$user][] = 'calendars/' . $user . '/' . $uri['uri'];
+ }
+
+ $properties = $this->propertyMapper->findPropertiesByPathsAndUsers($calendars);
+
+ $list = [];
+ foreach ($properties as $property) {
+ if ($property instanceof Property) {
+ if (!isset($list[$property->getPropertypath()])) {
+ $list[$property->getPropertypath()] = [];
+ }
+
+ $list[$property->getPropertypath()][$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 0c701d9cdcf..c0a313955bb 100644
--- a/apps/dav/lib/CalDAV/CalendarRoot.php
+++ b/apps/dav/lib/CalDAV/CalendarRoot.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -30,25 +12,29 @@ 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) {
- return new CalendarHome($this->caldavBackend, $principal, $this->logger);
+ return new CalendarHome(
+ $this->caldavBackend,
+ $principal,
+ $this->logger,
+ array_key_exists($principal['uri'], $this->returnCachedSubscriptions)
+ );
}
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];
@@ -56,4 +42,8 @@ class CalendarRoot extends \Sabre\CalDAV\CalendarRoot {
return parent::getName();
}
+
+ public function enableReturnCachedSubscriptions(string $principalUri): void {
+ $this->returnCachedSubscriptions['principals/users/' . $principalUri] = true;
+ }
}
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..d9d6d840c5e
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php
@@ -0,0 +1,122 @@
+<?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\Connector\Sabre\PropFindPreloadNotifyPlugin;
+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));
+ }
+
+ // collection preload plugin
+ $this->server->addPlugin(new PropFindPreloadNotifyPlugin());
+
+ // 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
new file mode 100644
index 00000000000..63395e7ce1c
--- /dev/null
+++ b/apps/dav/lib/CalDAV/EventComparisonService.php
@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV;
+
+use OCA\DAV\CalDAV\Schedule\IMipService;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+
+class EventComparisonService {
+
+ /** @var string[] */
+ private const EVENT_DIFF = [
+ 'RECURRENCE-ID',
+ 'RRULE',
+ 'SEQUENCE',
+ 'LAST-MODIFIED'
+ ];
+
+
+ /**
+ * If found, remove the event from $eventsToFilter that
+ * is identical to the passed $filterEvent
+ * and return whether an identical event was found
+ *
+ * This function takes into account the SEQUENCE,
+ * RRULE, RECURRENCE-ID and LAST-MODIFIED parameters
+ *
+ * @param VEvent $filterEvent
+ * @param array $eventsToFilter
+ * @return bool true if there was an identical event found and removed, false if there wasn't
+ */
+ private function removeIfUnchanged(VEvent $filterEvent, array &$eventsToFilter): bool {
+ $filterEventData = [];
+ 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) {
+ $eventToFilterData[] = IMipService::readPropertyWithDefault($eventToFilter, $eventDiff, '');
+ }
+ // events are identical and can be removed
+ if ($filterEventData === $eventToFilterData) {
+ unset($eventsToFilter[$k]);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Compare two VCalendars with each other and find all changed elements
+ *
+ * Returns an array of old and new events
+ *
+ * Old events are only detected if they are also changed
+ * If there is no corresponding old event for a VEvent, it
+ * has been newly created
+ *
+ * @param VCalendar $new
+ * @param VCalendar|null $old
+ * @return array<string, VEvent[]|null>
+ */
+ public function findModified(VCalendar $new, ?VCalendar $old): array {
+ $newEventComponents = $new->getComponents();
+
+ foreach ($newEventComponents as $k => $event) {
+ if (!$event instanceof VEvent) {
+ unset($newEventComponents[$k]);
+ }
+ }
+
+ if (empty($old)) {
+ return ['old' => null, 'new' => $newEventComponents];
+ }
+
+ $oldEventComponents = $old->getComponents();
+ if (is_array($oldEventComponents) && !empty($oldEventComponents)) {
+ foreach ($oldEventComponents as $k => $event) {
+ if (!$event instanceof VEvent) {
+ unset($oldEventComponents[$k]);
+ continue;
+ }
+ if ($this->removeIfUnchanged($event, $newEventComponents)) {
+ unset($oldEventComponents[$k]);
+ }
+ }
+ }
+
+ return ['old' => array_values($oldEventComponents), 'new' => array_values($newEventComponents)];
+ }
+}
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/FreeBusy/FreeBusyGenerator.php b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
new file mode 100644
index 00000000000..c2c474a90fe
--- /dev/null
+++ b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV\FreeBusy;
+
+use Sabre\VObject\Component\VCalendar;
+
+/**
+ * @psalm-suppress PropertyNotSetInConstructor
+ */
+class FreeBusyGenerator extends \Sabre\VObject\FreeBusyGenerator {
+
+ public function __construct() {
+ parent::__construct();
+ }
+
+ public function getVCalendar(): VCalendar {
+ return new VCalendar();
+ }
+}
diff --git a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
index ae568720c55..08dc10f7bf4 100644
--- a/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
+++ b/apps/dav/lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php
@@ -1,29 +1,13 @@
<?php
+
/**
- * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\ICSExportPlugin;
use OCP\IConfig;
-use OCP\ILogger;
+use Psr\Log\LoggerInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
@@ -35,24 +19,16 @@ use Sabre\VObject\Property\ICalendar\Duration;
* @package OCA\DAV\CalDAV\ICSExportPlugin
*/
class ICSExportPlugin extends \Sabre\CalDAV\ICSExportPlugin {
-
- /** @var IConfig */
- private $config;
-
- /** @var ILogger */
- private $logger;
-
/** @var string */
private const DEFAULT_REFRESH_INTERVAL = 'PT4H';
/**
* ICSExportPlugin constructor.
- *
- * @param IConfig $config
*/
- public function __construct(IConfig $config, ILogger $logger) {
- $this->config = $config;
- $this->logger = $logger;
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ ) {
}
/**
diff --git a/apps/dav/lib/CalDAV/IRestorable.php b/apps/dav/lib/CalDAV/IRestorable.php
index fab73c43d3a..5850e0a5645 100644
--- a/apps/dav/lib/CalDAV/IRestorable.php
+++ b/apps/dav/lib/CalDAV/IRestorable.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
diff --git a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
index 9c801a08a26..acf81638679 100644
--- a/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
+++ b/apps/dav/lib/CalDAV/Integration/ExternalCalendar.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright 2020, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Integration;
@@ -49,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,
+ ) {
}
/**
@@ -98,7 +77,7 @@ abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties {
* @return bool
*/
public static function isAppGeneratedCalendar(string $calendarUri):bool {
- return strpos($calendarUri, self::PREFIX) === 0 && substr_count($calendarUri, self::DELIMITER) >= 2;
+ return str_starts_with($calendarUri, self::PREFIX) && substr_count($calendarUri, self::DELIMITER) >= 2;
}
/**
@@ -126,6 +105,6 @@ abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties {
* @return bool
*/
public static function doesViolateReservedName(string $calendarUri):bool {
- return strpos($calendarUri, self::PREFIX) === 0;
+ return str_starts_with($calendarUri, self::PREFIX);
}
}
diff --git a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
index c72112f06ba..40a8860dcb4 100644
--- a/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
+++ b/apps/dav/lib/CalDAV/Integration/ICalendarProvider.php
@@ -1,24 +1,8 @@
<?php
+
/**
- * @copyright 2020, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Integration;
diff --git a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
index 5317dc1b169..c8a7109abde 100644
--- a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
+++ b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php
@@ -1,44 +1,35 @@
<?php
+
/**
- * @copyright Copyright (c) 2018, Georg Ehrke.
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
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;
class InvitationResponseServer {
-
/** @var \OCA\DAV\Connector\Sabre\Server */
public $server;
@@ -47,21 +38,23 @@ class InvitationResponseServer {
*/
public function __construct(bool $public = true) {
$baseUri = \OC::$WEBROOT . '/remote.php/dav/';
- $logger = \OC::$server->getLogger();
- /** @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
@@ -75,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
@@ -89,21 +82,21 @@ 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()));
+ $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
- $this->server->on('beforeMethod:*', function () use ($root) {
+ $this->server->on('beforeMethod:*', function () use ($root): void {
// 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);
@@ -126,7 +119,11 @@ class InvitationResponseServer {
public function isExternalAttendee(string $principalUri): bool {
/** @var \Sabre\DAVACL\Plugin $aclPlugin */
- $aclPlugin = $this->server->getPlugin('acl');
+ $aclPlugin = $this->getServer()->getPlugin('acl');
return $aclPlugin->getPrincipalByUri($principalUri) === null;
}
+
+ public function getServer(): \OCA\DAV\Connector\Sabre\Server {
+ return $this->server;
+ }
}
diff --git a/apps/dav/lib/CalDAV/Outbox.php b/apps/dav/lib/CalDAV/Outbox.php
index eebb48e1294..608114d8093 100644
--- a/apps/dav/lib/CalDAV/Outbox.php
+++ b/apps/dav/lib/CalDAV/Outbox.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2018, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -33,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;
@@ -45,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/Plugin.php b/apps/dav/lib/CalDAV/Plugin.php
index 5b367c51053..24448ae71ab 100644
--- a/apps/dav/lib/CalDAV/Plugin.php
+++ b/apps/dav/lib/CalDAV/Plugin.php
@@ -1,25 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud GmbH.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
diff --git a/apps/dav/lib/CalDAV/Principal/Collection.php b/apps/dav/lib/CalDAV/Principal/Collection.php
index 27997741609..b76fde66464 100644
--- a/apps/dav/lib/CalDAV/Principal/Collection.php
+++ b/apps/dav/lib/CalDAV/Principal/Collection.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Christoph Seitz <christoph.seitz@posteo.de>
- *
- * @author Christoph Seitz <christoph.seitz@posteo.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Principal;
diff --git a/apps/dav/lib/CalDAV/Principal/User.php b/apps/dav/lib/CalDAV/Principal/User.php
index 904ecc32e89..047d83827ed 100644
--- a/apps/dav/lib/CalDAV/Principal/User.php
+++ b/apps/dav/lib/CalDAV/Principal/User.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Christoph Seitz <christoph.seitz@posteo.de>
- *
- * @author Christoph Seitz <christoph.seitz@posteo.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Principal;
diff --git a/apps/dav/lib/CalDAV/Proxy/Proxy.php b/apps/dav/lib/CalDAV/Proxy/Proxy.php
index 8bafe8cc3b3..ef1ad8c634f 100644
--- a/apps/dav/lib/CalDAV/Proxy/Proxy.php
+++ b/apps/dav/lib/CalDAV/Proxy/Proxy.php
@@ -3,29 +3,13 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Proxy;
use OCP\AppFramework\Db\Entity;
+use OCP\DB\Types;
/**
* @method string getOwnerId()
@@ -45,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/Proxy/ProxyMapper.php b/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php
index 19c72ffa0e9..3b9b9c3d9eb 100644
--- a/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php
+++ b/apps/dav/lib/CalDAV/Proxy/ProxyMapper.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Proxy;
@@ -34,6 +15,8 @@ use OCP\IDBConnection;
* Class ProxyMapper
*
* @package OCA\DAV\CalDAV\Proxy
+ *
+ * @template-extends QBMapper<Proxy>
*/
class ProxyMapper extends QBMapper {
public const PERMISSION_READ = 1;
diff --git a/apps/dav/lib/CalDAV/PublicCalendar.php b/apps/dav/lib/CalDAV/PublicCalendar.php
index 4a29c8d237a..9af6e544165 100644
--- a/apps/dav/lib/CalDAV/PublicCalendar.php
+++ b/apps/dav/lib/CalDAV/PublicCalendar.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Georg Ehrke
- *
- * @author Gary Kim <gary@garykim.dev>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -84,7 +66,7 @@ class PublicCalendar extends Calendar {
* public calendars are always shared
* @return bool
*/
- protected function isShared() {
+ public function isShared() {
return true;
}
}
diff --git a/apps/dav/lib/CalDAV/PublicCalendarObject.php b/apps/dav/lib/CalDAV/PublicCalendarObject.php
index 69a5583d8f5..2ab40b94347 100644
--- a/apps/dav/lib/CalDAV/PublicCalendarObject.php
+++ b/apps/dav/lib/CalDAV/PublicCalendarObject.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
diff --git a/apps/dav/lib/CalDAV/PublicCalendarRoot.php b/apps/dav/lib/CalDAV/PublicCalendarRoot.php
index 4f7dfea2682..edfb9f8dccc 100644
--- a/apps/dav/lib/CalDAV/PublicCalendarRoot.php
+++ b/apps/dav/lib/CalDAV/PublicCalendarRoot.php
@@ -1,27 +1,9 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Lukas Reschke <lukas@statuscode.ch>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -32,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.
*
@@ -51,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 97e942f9da2..76378e7a1c5 100644
--- a/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php
+++ b/apps/dav/lib/CalDAV/Publishing/PublishPlugin.php
@@ -1,34 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
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;
@@ -51,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,
+ ) {
}
/**
@@ -114,28 +87,28 @@ class PublishPlugin extends ServerPlugin {
$this->server = $server;
$this->server->on('method:POST', [$this, 'httpPost']);
- $this->server->on('propFind', [$this, 'propFind']);
+ $this->server->on('propFind', [$this, 'propFind']);
}
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());
if ($this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes') {
- $canShare &= ($node->getOwner() === $node->getPrincipalURI());
- $canPublish &= ($node->getOwner() === $node->getPrincipalURI());
+ $canShare = $canShare && ($node->getOwner() === $node->getPrincipalURI());
+ $canPublish = $canPublish && ($node->getOwner() === $node->getPrincipalURI());
}
return new AllowedSharingModes($canShare, $canPublish);
@@ -155,8 +128,8 @@ class PublishPlugin extends ServerPlugin {
$path = $request->getPath();
// Only handling xml
- $contentType = $request->getHeader('Content-Type');
- if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) {
+ $contentType = (string)$request->getHeader('Content-Type');
+ if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
@@ -182,74 +155,74 @@ 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) {
- return;
- }
- $this->server->transactionType = 'post-publish-calendar';
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof Calendar) {
+ return;
+ }
+ $this->server->transactionType = 'post-publish-calendar';
- // Getting ACL info
- $acl = $this->server->getPlugin('acl');
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
- // If there's no ACL support, we allow everything
- if ($acl) {
- /** @var \Sabre\DAVACL\Plugin $acl */
- $acl->checkPrivileges($path, '{DAV:}write');
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ /** @var \Sabre\DAVACL\Plugin $acl */
+ $acl->checkPrivileges($path, '{DAV:}write');
- $limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
- $isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
- if ($limitSharingToOwner && !$isOwner) {
- return;
+ $limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
+ $isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
+ if ($limitSharingToOwner && !$isOwner) {
+ return;
+ }
}
- }
- $node->setPublishStatus(true);
+ $node->setPublishStatus(true);
- // iCloud sends back the 202, so we will too.
- $response->setStatus(202);
+ // iCloud sends back the 202, so we will too.
+ $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.
- $response->setHeader('X-Sabre-Status', 'everything-went-well');
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
- // Breaking the event chain
- return false;
+ // 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) {
- return;
- }
- $this->server->transactionType = 'post-unpublish-calendar';
+ // We can only deal with IShareableCalendar objects
+ if (!$node instanceof Calendar) {
+ return;
+ }
+ $this->server->transactionType = 'post-unpublish-calendar';
- // Getting ACL info
- $acl = $this->server->getPlugin('acl');
+ // Getting ACL info
+ $acl = $this->server->getPlugin('acl');
- // If there's no ACL support, we allow everything
- if ($acl) {
- /** @var \Sabre\DAVACL\Plugin $acl */
- $acl->checkPrivileges($path, '{DAV:}write');
+ // If there's no ACL support, we allow everything
+ if ($acl) {
+ /** @var \Sabre\DAVACL\Plugin $acl */
+ $acl->checkPrivileges($path, '{DAV:}write');
- $limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
- $isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
- if ($limitSharingToOwner && !$isOwner) {
- return;
+ $limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
+ $isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
+ if ($limitSharingToOwner && !$isOwner) {
+ return;
+ }
}
- }
- $node->setPublishStatus(false);
+ $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.
- $response->setHeader('X-Sabre-Status', 'everything-went-well');
+ // Adding this because sending a response body may cause issues,
+ // and I wanted some type of indicator the response was handled.
+ $response->setHeader('X-Sabre-Status', 'everything-went-well');
- // Breaking the event chain
- return false;
+ // Breaking the event chain
+ return false;
}
}
diff --git a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
index 35bce872bf8..fb9b7298f9b 100644
--- a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
+++ b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php
@@ -1,25 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Publishing\Xml;
@@ -29,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,
+ ) {
}
/**
@@ -55,7 +29,7 @@ class Publisher implements XmlSerializable {
}
/**
- * The xmlSerialize metod is called during xml writing.
+ * The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
diff --git a/apps/dav/lib/CalDAV/Reminder/Backend.php b/apps/dav/lib/CalDAV/Reminder/Backend.php
index b0476e9594c..329af3a2f56 100644
--- a/apps/dav/lib/CalDAV/Reminder/Backend.php
+++ b/apps/dav/lib/CalDAV/Reminder/Backend.php
@@ -3,28 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
@@ -38,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,
+ ) {
}
/**
@@ -64,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'],
@@ -88,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'],
@@ -114,17 +89,17 @@ class Backend {
* @return int The insert id
*/
public function insertReminder(int $calendarId,
- int $objectId,
- string $uid,
- bool $isRecurring,
- int $recurrenceId,
- bool $isRecurrenceException,
- string $eventHash,
- string $alarmHash,
- string $type,
- bool $isRelative,
- int $notificationDate,
- bool $isRepeatBased):int {
+ int $objectId,
+ string $uid,
+ bool $isRecurring,
+ int $recurrenceId,
+ bool $isRecurrenceException,
+ string $eventHash,
+ string $alarmHash,
+ string $type,
+ bool $isRelative,
+ int $notificationDate,
+ bool $isRepeatBased):int {
$query = $this->db->getQueryBuilder();
$query->insert('calendar_reminders')
->values([
@@ -141,7 +116,7 @@ class Backend {
'notification_date' => $query->createNamedParameter($notificationDate),
'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0),
])
- ->execute();
+ ->executeStatement();
return $query->getLastInsertId();
}
@@ -153,12 +128,12 @@ class Backend {
* @param int $newNotificationDate
*/
public function updateReminder(int $reminderId,
- int $newNotificationDate):void {
+ int $newNotificationDate):void {
$query = $this->db->getQueryBuilder();
$query->update('calendar_reminders')
->set('notification_date', $query->createNamedParameter($newNotificationDate))
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -172,7 +147,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -185,7 +160,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -199,7 +174,7 @@ class Backend {
$query->delete('calendar_reminders')
->where($query->expr()->eq('calendar_id', $query->createNamedParameter($calendarId)))
- ->execute();
+ ->executeStatement();
}
/**
@@ -207,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 a6b439c0b4f..31d60f1531d 100644
--- a/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
@@ -3,27 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
@@ -41,11 +22,13 @@ interface INotificationProvider {
* Send notification
*
* @param VEvent $vevent
- * @param string $calendarDisplayName
+ * @param string|null $calendarDisplayName
+ * @param string[] $principalEmailAddresses All email addresses associated to the principal owning the calendar object
* @param IUser[] $users
* @return void
*/
public function send(VEvent $vevent,
- string $calendarDisplayName,
- array $users = []): void;
+ ?string $calendarDisplayName,
+ 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 044e5fac4e2..94edff98e52 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
@@ -3,39 +3,18 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
use OCA\DAV\CalDAV\Reminder\INotificationProvider;
use OCP\IConfig;
use OCP\IL10N;
-use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory as L10NFactory;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\Property;
@@ -50,51 +29,33 @@ abstract class AbstractProvider implements INotificationProvider {
/** @var string */
public const NOTIFICATION_TYPE = '';
- /** @var ILogger */
- protected $logger;
-
- /** @var L10NFactory */
- protected $l10nFactory;
-
/** @var IL10N[] */
private $l10ns;
/** @var string */
private $fallbackLanguage;
- /** @var IURLGenerator */
- protected $urlGenerator;
-
- /** @var IConfig */
- protected $config;
-
- /**
- * @param ILogger $logger
- * @param L10NFactory $l10nFactory
- * @param IConfig $config
- * @param IUrlGenerator $urlGenerator
- */
- public function __construct(ILogger $logger,
- L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator,
- IConfig $config) {
- $this->logger = $logger;
- $this->l10nFactory = $l10nFactory;
- $this->urlGenerator = $urlGenerator;
- $this->config = $config;
+ public function __construct(
+ protected LoggerInterface $logger,
+ protected L10NFactory $l10nFactory,
+ protected IURLGenerator $urlGenerator,
+ protected IConfig $config,
+ ) {
}
/**
* Send notification
*
* @param VEvent $vevent
- * @param string $calendarDisplayName
+ * @param string|null $calendarDisplayName
+ * @param string[] $principalEmailAddresses
* @param IUser[] $users
* @return void
*/
abstract public function send(VEvent $vevent,
- string $calendarDisplayName,
- array $users = []): void;
+ ?string $calendarDisplayName,
+ array $principalEmailAddresses,
+ array $users = []): void;
/**
* @return string
@@ -139,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,
@@ -189,4 +150,8 @@ abstract class AbstractProvider implements INotificationProvider {
return clone $vevent->DTSTART;
}
+
+ protected function getCalendarDisplayNameFallback(string $lang): string {
+ return $this->getL10NForLang($lang)->t('Untitled calendar');
+ }
}
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php
index 4b369b34dc0..01d51489a3b 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/AudioProvider.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
index 456b9f8b42d..0fd39a9e459 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
@@ -3,42 +3,22 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Richard Steinmetz <richard@steinmetz.cloud>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
use DateTime;
use OCP\IConfig;
use OCP\IL10N;
-use OCP\ILogger;
use OCP\IURLGenerator;
+use OCP\IUser;
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;
use Sabre\VObject\Parameter;
@@ -50,44 +30,46 @@ use Sabre\VObject\Property;
* @package OCA\DAV\CalDAV\Reminder\NotificationProvider
*/
class EmailProvider extends AbstractProvider {
-
/** @var string */
public const NOTIFICATION_TYPE = 'EMAIL';
- /** @var IMailer */
- private $mailer;
-
- /**
- * @param IConfig $config
- * @param IMailer $mailer
- * @param ILogger $logger
- * @param L10NFactory $l10nFactory
- * @param IUrlGenerator $urlGenerator
- */
- public function __construct(IConfig $config,
- IMailer $mailer,
- ILogger $logger,
- L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator) {
+ public function __construct(
+ IConfig $config,
+ private IMailer $mailer,
+ LoggerInterface $logger,
+ L10NFactory $l10nFactory,
+ IURLGenerator $urlGenerator,
+ ) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
- $this->mailer = $mailer;
}
/**
* Send out notification via email
*
* @param VEvent $vevent
- * @param string $calendarDisplayName
+ * @param string|null $calendarDisplayName
+ * @param string[] $principalEmailAddresses
* @param array $users
* @throws \Exception
*/
public function send(VEvent $vevent,
- string $calendarDisplayName,
- array $users = []):void {
+ ?string $calendarDisplayName,
+ array $principalEmailAddresses,
+ array $users = []):void {
$fallbackLanguage = $this->getFallbackLanguage();
+ $organizerEmailAddress = null;
+ if (isset($vevent->ORGANIZER)) {
+ $organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
+ }
+
$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
- $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
+ $emailAddressesOfAttendees = [];
+ if (count($principalEmailAddresses) === 0
+ || ($organizerEmailAddress && in_array($organizerEmailAddress, $principalEmailAddresses, true))
+ ) {
+ $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
+ }
// Quote from php.net:
// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
@@ -105,12 +87,12 @@ 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();
$this->addSubjectAndHeading($template, $l10n, $vevent);
- $this->addBulletList($template, $l10n, $calendarDisplayName, $vevent);
+ $this->addBulletList($template, $l10n, $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($lang), $vevent);
$template->addFooter();
foreach ($emailAddresses as $emailAddress) {
@@ -126,6 +108,7 @@ class EmailProvider extends AbstractProvider {
}
$message->setTo([$emailAddress]);
$message->useTemplate($template);
+ $message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
try {
$failed = $this->mailer->send($message);
@@ -133,7 +116,7 @@ class EmailProvider extends AbstractProvider {
$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
}
} catch (\Exception $ex) {
- $this->logger->logException($ex, ['app' => 'dav']);
+ $this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
}
}
}
@@ -156,9 +139,9 @@ class EmailProvider extends AbstractProvider {
* @param array $eventData
*/
private function addBulletList(IEMailTemplate $template,
- IL10N $l10n,
- string $calendarDisplayName,
- VEvent $vevent):void {
+ IL10N $l10n,
+ string $calendarDisplayName,
+ VEvent $vevent):void {
$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
$this->getAbsoluteImagePath('actions/info.png'));
@@ -166,19 +149,15 @@ 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'));
}
}
- /**
- * @param string $path
- * @return string
- */
private function getAbsoluteImagePath(string $path):string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->imagePath('core', $path)
@@ -201,7 +180,7 @@ class EmailProvider extends AbstractProvider {
$organizerEMail = substr($organizer->getValue(), 7);
- if ($organizerEMail === false || !$this->mailer->validateMailAddress($organizerEMail)) {
+ if (!$this->mailer->validateMailAddress($organizerEMail)) {
return null;
}
@@ -214,12 +193,11 @@ class EmailProvider extends AbstractProvider {
}
/**
- * @param array $emails
- * @param string $defaultLanguage
- * @return array
+ * @param array<string, array{LANG?: string}> $emails
+ * @return array<string, string[]>
*/
private function sortEMailAddressesByLanguage(array $emails,
- string $defaultLanguage):array {
+ string $defaultLanguage):array {
$sortedByLanguage = [];
foreach ($emails as $emailAddress => $parameters) {
@@ -241,7 +219,7 @@ class EmailProvider extends AbstractProvider {
/**
* @param VEvent $vevent
- * @return array
+ * @return array<string, array{LANG?: string}>
*/
private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
$emailAddresses = [];
@@ -272,7 +250,10 @@ class EmailProvider extends AbstractProvider {
$emailAddressesOfDelegates = $delegates->getParts();
foreach ($emailAddressesOfDelegates as $addressesOfDelegate) {
if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
- $emailAddresses[substr($addressesOfDelegate, 7)] = [];
+ $delegateEmail = substr($addressesOfDelegate, 7);
+ if ($this->mailer->validateMailAddress($delegateEmail)) {
+ $emailAddresses[$delegateEmail] = [];
+ }
}
}
@@ -284,7 +265,7 @@ class EmailProvider extends AbstractProvider {
$properties = [];
$langProp = $attendee->offsetGet('LANG');
- if ($langProp instanceof VObject\Parameter) {
+ if ($langProp instanceof VObject\Parameter && $langProp->getValue() !== null) {
$properties['LANG'] = $langProp->getValue();
}
@@ -294,18 +275,15 @@ class EmailProvider extends AbstractProvider {
}
if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
- $emailAddresses[$this->getEMailAddressOfAttendee($vevent->ORGANIZER)] = [];
+ $organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
+ if ($organizerEmailAddress !== null) {
+ $emailAddresses[$organizerEmailAddress] = [];
+ }
}
return $emailAddresses;
}
-
-
- /**
- * @param VObject\Property $attendee
- * @return string
- */
private function getCUTypeOfAttendee(VObject\Property $attendee):string {
$cuType = $attendee->offsetGet('CUTYPE');
if ($cuType instanceof VObject\Parameter) {
@@ -315,10 +293,6 @@ class EmailProvider extends AbstractProvider {
return 'INDIVIDUAL';
}
- /**
- * @param VObject\Property $attendee
- * @return string
- */
private function getPartstatOfAttendee(VObject\Property $attendee):string {
$partstat = $attendee->offsetGet('PARTSTAT');
if ($partstat instanceof VObject\Parameter) {
@@ -328,29 +302,25 @@ class EmailProvider extends AbstractProvider {
return 'NEEDS-ACTION';
}
- /**
- * @param VObject\Property $attendee
- * @return bool
- */
- private function hasAttendeeMailURI(VObject\Property $attendee):bool {
+ private function hasAttendeeMailURI(VObject\Property $attendee): bool {
return stripos($attendee->getValue(), 'mailto:') === 0;
}
- /**
- * @param VObject\Property $attendee
- * @return string|null
- */
- private function getEMailAddressOfAttendee(VObject\Property $attendee):?string {
+ private function getEMailAddressOfAttendee(VObject\Property $attendee): ?string {
if (!$this->hasAttendeeMailURI($attendee)) {
return null;
}
+ $attendeeEMail = substr($attendee->getValue(), 7);
+ if (!$this->mailer->validateMailAddress($attendeeEMail)) {
+ return null;
+ }
- return substr($attendee->getValue(), 7);
+ return $attendeeEMail;
}
/**
- * @param array $users
- * @return array
+ * @param IUser[] $users
+ * @return array<string, array{LANG?: string}>
*/
private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
$emailAddresses = [];
@@ -373,12 +343,9 @@ class EmailProvider extends AbstractProvider {
}
/**
- * @param IL10N $l10n
- * @param VEvent $vevent
- * @return string
* @throws \Exception
*/
- private function generateDateString(IL10N $l10n, VEvent $vevent):string {
+ private function generateDateString(IL10N $l10n, VEvent $vevent): string {
$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
@@ -444,57 +411,27 @@ class EmailProvider extends AbstractProvider {
. ' (' . $startTimezone . ')';
}
- /**
- * @param DateTime $dtStart
- * @param DateTime $dtEnd
- * @return bool
- */
private function isDayEqual(DateTime $dtStart,
- DateTime $dtEnd):bool {
+ DateTime $dtEnd):bool {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
}
- /**
- * @param IL10N $l10n
- * @param DateTime $dt
- * @return string
- */
private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
- return $l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
+ return (string)$l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
}
- /**
- * @param IL10N $l10n
- * @param DateTime $dt
- * @return string
- */
private function getDateString(IL10N $l10n, DateTime $dt):string {
- return $l10n->l('date', $dt, ['width' => 'medium']);
+ return (string)$l10n->l('date', $dt, ['width' => 'medium']);
}
- /**
- * @param IL10N $l10n
- * @param DateTime $dt
- * @return string
- */
private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
- return $l10n->l('datetime', $dt, ['width' => 'medium|short']);
+ return (string)$l10n->l('datetime', $dt, ['width' => 'medium|short']);
}
- /**
- * @param IL10N $l10n
- * @param DateTime $dt
- * @return string
- */
private function getTimeString(IL10N $l10n, DateTime $dt):string {
- return $l10n->l('time', $dt, ['width' => 'short']);
+ return (string)$l10n->l('time', $dt, ['width' => 'short']);
}
- /**
- * @param VEvent $vevent
- * @param IL10N $l10n
- * @return string
- */
private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
if (isset($vevent->SUMMARY)) {
return (string)$vevent->SUMMARY;
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php
index 2e4f9a38493..15994bacf49 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/ProviderNotAvailableException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr>
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
diff --git a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
index fb123960df8..a3f0cce547a 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
@@ -3,41 +3,20 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
use OCA\DAV\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
-use OCP\ILogger;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory as L10NFactory;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
+use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Property;
@@ -51,55 +30,44 @@ class PushProvider extends AbstractProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'DISPLAY';
- /** @var IManager */
- private $manager;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /**
- * @param IConfig $config
- * @param IManager $manager
- * @param ILogger $logger
- * @param L10NFactory $l10nFactory
- * @param IUrlGenerator $urlGenerator
- * @param ITimeFactory $timeFactory
- */
- public function __construct(IConfig $config,
- IManager $manager,
- ILogger $logger,
- L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator,
- ITimeFactory $timeFactory) {
+ public function __construct(
+ IConfig $config,
+ private IManager $manager,
+ LoggerInterface $logger,
+ L10NFactory $l10nFactory,
+ IURLGenerator $urlGenerator,
+ private ITimeFactory $timeFactory,
+ ) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
- $this->manager = $manager;
- $this->timeFactory = $timeFactory;
}
/**
* Send push notification to all users.
*
* @param VEvent $vevent
- * @param string $calendarDisplayName
+ * @param string|null $calendarDisplayName
+ * @param string[] $principalEmailAddresses
* @param IUser[] $users
* @throws \Exception
*/
public function send(VEvent $vevent,
- string $calendarDisplayName = null,
- array $users = []):void {
- if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'no') !== 'yes') {
+ ?string $calendarDisplayName,
+ array $principalEmailAddresses,
+ array $users = []):void {
+ if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'yes') !== 'yes') {
return;
}
$eventDetails = $this->extractEventDetails($vevent);
- $eventDetails['calendar_displayname'] = $calendarDisplayName;
- $eventUUID = (string) $vevent->UID;
+ $eventUUID = (string)$vevent->UID;
if (!$eventUUID) {
return;
};
$eventUUIDHash = hash('sha256', $eventUUID, false);
foreach ($users as $user) {
+ $eventDetails['calendar_displayname'] = $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($this->l10nFactory->getUserLanguage($user));
+
/** @var INotification $notification */
$notification = $this->manager->createNotification();
$notification->setApp(Application::APP_ID)
@@ -117,8 +85,6 @@ class PushProvider extends AbstractProvider {
}
/**
- * @var VEvent $vevent
- * @return array
* @throws \Exception
*/
protected function extractEventDetails(VEvent $vevent):array {
@@ -128,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 cd8030a1177..265db09b061 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationProviderManager.php
@@ -3,30 +3,15 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
+use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException;
+use OCP\AppFramework\QueryException;
+use OCP\Server;
+
/**
* Class NotificationProviderManager
*
@@ -61,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);
}
@@ -70,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/NotificationTypeDoesNotExistException.php b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php
index 16fb858bc3a..6fd2a29ede5 100644
--- a/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php
+++ b/apps/dav/lib/CalDAV/Reminder/NotificationTypeDoesNotExistException.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- *
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
diff --git a/apps/dav/lib/CalDAV/Reminder/Notifier.php b/apps/dav/lib/CalDAV/Reminder/Notifier.php
index 8535c55054a..137fb509f56 100644
--- a/apps/dav/lib/CalDAV/Reminder/Notifier.php
+++ b/apps/dav/lib/CalDAV/Reminder/Notifier.php
@@ -3,29 +3,8 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
@@ -38,6 +17,7 @@ use OCP\L10N\IFactory;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
+use OCP\Notification\UnknownNotificationException;
/**
* Class Notifier
@@ -46,31 +26,21 @@ use OCP\Notification\INotifier;
*/
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,
+ ) {
}
/**
@@ -99,12 +69,12 @@ class Notifier implements INotifier {
* @param INotification $notification
* @param string $languageCode The code of the language that should be used to prepare the notification
* @return INotification
- * @throws \Exception
+ * @throws UnknownNotificationException
*/
public function prepare(INotification $notification,
- string $languageCode):INotification {
+ string $languageCode):INotification {
if ($notification->getApp() !== Application::APP_ID) {
- throw new \InvalidArgumentException('Notification not from this app');
+ throw new UnknownNotificationException('Notification not from this app');
}
// Read the language from the notification
@@ -116,7 +86,7 @@ class Notifier implements INotifier {
return $this->prepareReminderNotification($notification);
default:
- throw new \InvalidArgumentException('Unknown subject');
+ throw new UnknownNotificationException('Unknown subject');
}
}
@@ -170,21 +140,35 @@ class Notifier implements INotifier {
$components[] = $this->l10n->n('%n minute', '%n minutes', $diff->i);
}
- // Limiting to the first three components to prevent
- // the string from getting too long
- $firstThreeComponents = array_slice($components, 0, 2);
- $diffLabel = implode(', ', $firstThreeComponents);
+ if (count($components) > 0 && !$this->hasPhpDatetimeDiffBug()) {
+ // Limiting to the first three components to prevent
+ // the string from getting too long
+ $firstThreeComponents = array_slice($components, 0, 2);
+ $diffLabel = implode(', ', $firstThreeComponents);
- if ($diff->invert) {
- $title = $this->l10n->t('%s (in %s)', [$title, $diffLabel]);
- } else {
- $title = $this->l10n->t('%s (%s ago)', [$title, $diffLabel]);
+ if ($diff->invert) {
+ $title = $this->l10n->t('%s (in %s)', [$title, $diffLabel]);
+ } else {
+ $title = $this->l10n->t('%s (%s ago)', [$title, $diffLabel]);
+ }
}
$notification->setParsedSubject($title);
}
/**
+ * @see https://github.com/nextcloud/server/issues/41615
+ * @see https://github.com/php/php-src/issues/9699
+ */
+ private function hasPhpDatetimeDiffBug(): bool {
+ $d1 = DateTime::createFromFormat(\DateTimeInterface::ATOM, '2023-11-22T11:52:00+01:00');
+ $d2 = new DateTime('2023-11-22T10:52:03', new \DateTimeZone('UTC'));
+
+ // The difference is 3 seconds, not -1year+11months+…
+ return $d1->diff($d2)->y < 0;
+ }
+
+ /**
* Sets the notification message based on the parameters set in PushProvider
*
* @param INotification $notification
@@ -289,7 +273,7 @@ class Notifier implements INotifier {
* @return bool
*/
private function isDayEqual(DateTime $dtStart,
- DateTime $dtEnd):bool {
+ DateTime $dtEnd):bool {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
}
@@ -298,7 +282,7 @@ class Notifier implements INotifier {
* @return string
*/
private function getWeekDayName(DateTime $dt):string {
- return $this->l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
+ return (string)$this->l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
}
/**
@@ -306,7 +290,7 @@ class Notifier implements INotifier {
* @return string
*/
private function getDateString(DateTime $dt):string {
- return $this->l10n->l('date', $dt, ['width' => 'medium']);
+ return (string)$this->l10n->l('date', $dt, ['width' => 'medium']);
}
/**
@@ -314,7 +298,7 @@ class Notifier implements INotifier {
* @return string
*/
private function getDateTimeString(DateTime $dt):string {
- return $this->l10n->l('datetime', $dt, ['width' => 'medium|short']);
+ return (string)$this->l10n->l('datetime', $dt, ['width' => 'medium|short']);
}
/**
@@ -322,6 +306,6 @@ class Notifier implements INotifier {
* @return string
*/
private function getTimeString(DateTime $dt):string {
- return $this->l10n->l('time', $dt, ['width' => 'short']);
+ return (string)$this->l10n->l('time', $dt, ['width' => 'short']);
}
}
diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php
index d6901cc4fb0..c75090e1560 100644
--- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php
+++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php
@@ -3,73 +3,35 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2019, Thomas Citharel
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Reminder;
use DateTimeImmutable;
+use DateTimeZone;
use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Connector\Sabre\Principal;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
use Sabre\VObject;
use Sabre\VObject\Component\VAlarm;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\ParseException;
use Sabre\VObject\Recur\EventIterator;
+use Sabre\VObject\Recur\MaxInstancesExceededException;
use Sabre\VObject\Recur\NoInstancesException;
+use function count;
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;
-
public const REMINDER_TYPE_EMAIL = 'EMAIL';
public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
public const REMINDER_TYPE_AUDIO = 'AUDIO';
@@ -85,31 +47,17 @@ class ReminderService {
self::REMINDER_TYPE_AUDIO
];
- /**
- * ReminderService constructor.
- *
- * @param Backend $backend
- * @param NotificationProviderManager $notificationProviderManager
- * @param IUserManager $userManager
- * @param IGroupManager $groupManager
- * @param CalDavBackend $caldavBackend
- * @param ITimeFactory $timeFactory
- * @param IConfig $config
- */
- public function __construct(Backend $backend,
- NotificationProviderManager $notificationProviderManager,
- IUserManager $userManager,
- IGroupManager $groupManager,
- CalDavBackend $caldavBackend,
- ITimeFactory $timeFactory,
- IConfig $config) {
- $this->backend = $backend;
- $this->notificationProviderManager = $notificationProviderManager;
- $this->userManager = $userManager;
- $this->groupManager = $groupManager;
- $this->caldavBackend = $caldavBackend;
- $this->timeFactory = $timeFactory;
- $this->config = $config;
+ 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,
+ ) {
}
/**
@@ -118,8 +66,11 @@ class ReminderService {
* @throws NotificationProvider\ProviderNotAvailableException
* @throws NotificationTypeDoesNotExistException
*/
- public function processReminders():void {
+ public function processReminders() :void {
$reminders = $this->backend->getRemindersToProcess();
+ $this->logger->debug('{numReminders} reminders to process', [
+ 'numReminders' => count($reminders),
+ ]);
foreach ($reminders as $reminder) {
$calendarData = is_resource($reminder['calendardata'])
@@ -132,27 +83,46 @@ class ReminderService {
$vcalendar = $this->parseCalendarData($calendarData);
if (!$vcalendar) {
+ $this->logger->debug('Reminder {id} does not belong to a valid calendar', [
+ 'id' => $reminder['id'],
+ ]);
+ $this->backend->removeReminder($reminder['id']);
+ continue;
+ }
+
+ try {
+ $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
+ } catch (MaxInstancesExceededException $e) {
+ $this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]);
$this->backend->removeReminder($reminder['id']);
continue;
}
- $vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
if (!$vevent) {
+ $this->logger->debug('Reminder {id} does not belong to a valid event', [
+ 'id' => $reminder['id'],
+ ]);
$this->backend->removeReminder($reminder['id']);
continue;
}
if ($this->wasEventCancelled($vevent)) {
+ $this->logger->debug('Reminder {id} belongs to a cancelled event', [
+ 'id' => $reminder['id'],
+ ]);
$this->deleteOrProcessNext($reminder, $vevent);
continue;
}
if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
+ $this->logger->debug('Reminder {id} does not belong to a valid notification provider', [
+ 'id' => $reminder['id'],
+ ]);
$this->deleteOrProcessNext($reminder, $vevent);
continue;
}
- if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedGroupMembers', 'yes') === 'no') {
+ if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'no') {
$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
} else {
$users = [];
@@ -163,8 +133,18 @@ class ReminderService {
$users[] = $user;
}
+ $userPrincipalEmailAddresses = [];
+ $userPrincipal = $this->principalConnector->getPrincipalByPath($reminder['principaluri']);
+ if ($userPrincipal) {
+ $userPrincipalEmailAddresses = $this->principalConnector->getEmailAddressesOfPrincipal($userPrincipal);
+ }
+
+ $this->logger->debug('Reminder {id} will be sent to {numUsers} users', [
+ 'id' => $reminder['id'],
+ 'numUsers' => count($users),
+ ]);
$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
- $notificationProvider->send($vevent, $reminder['displayname'], $users);
+ $notificationProvider->send($vevent, $reminder['displayname'], $userPrincipalEmailAddresses, $users);
$this->deleteOrProcessNext($reminder, $vevent);
}
@@ -188,18 +168,18 @@ class ReminderService {
return;
}
- /** @var VObject\Component\VCalendar $vcalendar */
$vcalendar = $this->parseCalendarData($calendarData);
if (!$vcalendar) {
return;
}
+ $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();
@@ -221,7 +201,7 @@ class ReminderService {
continue;
}
- $alarms = $this->getRemindersForVAlarm($valarm, $objectData,
+ $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone,
$eventHash, $alarmHash, true, true);
$this->writeRemindersToDatabase($alarms);
}
@@ -247,6 +227,10 @@ class ReminderService {
// instance. We are skipping this event from the output
// entirely.
return;
+ } catch (MaxInstancesExceededException $e) {
+ // The event has more than 3500 recurring-instances
+ // so we can ignore it
+ return;
}
while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
@@ -265,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;
@@ -274,6 +258,16 @@ class ReminderService {
try {
$triggerTime = $valarm->getEffectiveTriggerTime();
+ /**
+ * @psalm-suppress DocblockTypeContradiction
+ * https://github.com/vimeo/psalm/issues/9244
+ */
+ if ($triggerTime->getTimezone() === false || $triggerTime->getTimezone()->getName() === 'UTC') {
+ $triggerTime = new DateTimeImmutable(
+ $triggerTime->format('Y-m-d H:i:s'),
+ $calendarTimeZone
+ );
+ }
} catch (InvalidDataException $e) {
continue;
}
@@ -292,7 +286,7 @@ class ReminderService {
continue;
}
- $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $masterHash, $alarmHash, $isRecurring, false);
+ $alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $masterHash, $alarmHash, $isRecurring, false);
$this->writeRemindersToDatabase($alarms);
$processedAlarms[] = $alarmHash;
}
@@ -325,12 +319,13 @@ class ReminderService {
return;
}
- $this->backend->cleanRemindersForEvent((int) $objectData['id']);
+ $this->backend->cleanRemindersForEvent((int)$objectData['id']);
}
/**
* @param VAlarm $valarm
* @param array $objectData
+ * @param DateTimeZone $calendarTimeZone
* @param string|null $eventHash
* @param string|null $alarmHash
* @param bool $isRecurring
@@ -338,11 +333,12 @@ class ReminderService {
* @return array
*/
private function getRemindersForVAlarm(VAlarm $valarm,
- array $objectData,
- string $eventHash = null,
- string $alarmHash = null,
- bool $isRecurring = false,
- bool $isRecurrenceException = false):array {
+ array $objectData,
+ DateTimeZone $calendarTimeZone,
+ ?string $eventHash = null,
+ ?string $alarmHash = null,
+ bool $isRecurring = false,
+ bool $isRecurrenceException = false):array {
if ($eventHash === null) {
$eventHash = $this->getEventHash($valarm->parent);
}
@@ -354,6 +350,16 @@ class ReminderService {
$isRelative = $this->isAlarmRelative($valarm);
/** @var DateTimeImmutable $notificationDate */
$notificationDate = $valarm->getEffectiveTriggerTime();
+ /**
+ * @psalm-suppress DocblockTypeContradiction
+ * https://github.com/vimeo/psalm/issues/9244
+ */
+ if ($notificationDate->getTimezone() === false || $notificationDate->getTimezone()->getName() === 'UTC') {
+ $notificationDate = new DateTimeImmutable(
+ $notificationDate->format('Y-m-d H:i:s'),
+ $calendarTimeZone
+ );
+ }
$clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
$clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
@@ -362,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;
@@ -384,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,
@@ -404,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']
);
}
@@ -427,11 +440,11 @@ class ReminderService {
* @param VEvent $vevent
*/
private function deleteOrProcessNext(array $reminder,
- VObject\Component\VEvent $vevent):void {
- if ($reminder['is_repeat_based'] ||
- !$reminder['is_recurring'] ||
- !$reminder['is_relative'] ||
- $reminder['is_recurrence_exception']) {
+ VObject\Component\VEvent $vevent):void {
+ if ($reminder['is_repeat_based']
+ || !$reminder['is_recurring']
+ || !$reminder['is_relative']
+ || $reminder['is_recurrence_exception']) {
$this->backend->removeReminder($reminder['id']);
return;
}
@@ -439,6 +452,7 @@ class ReminderService {
$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$now = $this->timeFactory->getDateTime();
+ $calendarTimeZone = $this->getCalendarTimeZone((int)$reminder['calendar_id']);
try {
$iterator = new EventIterator($vevents, $reminder['uid']);
@@ -449,49 +463,54 @@ class ReminderService {
return;
}
- while ($iterator->valid()) {
- $event = $iterator->getEventObject();
-
- // Recurrence-exceptions are handled separately, so just ignore them here
- if (\in_array($event, $recurrenceExceptions, true)) {
- $iterator->next();
- continue;
- }
-
- $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
- if ($reminder['recurrence_id'] >= $recurrenceId) {
- $iterator->next();
- continue;
- }
+ try {
+ while ($iterator->valid()) {
+ $event = $iterator->getEventObject();
- foreach ($event->VALARM as $valarm) {
- /** @var VAlarm $valarm */
- $alarmHash = $this->getAlarmHash($valarm);
- if ($alarmHash !== $reminder['alarm_hash']) {
+ // Recurrence-exceptions are handled separately, so just ignore them here
+ if (\in_array($event, $recurrenceExceptions, true)) {
+ $iterator->next();
continue;
}
- $triggerTime = $valarm->getEffectiveTriggerTime();
-
- // If effective trigger time is in the past
- // just skip and generate for next event
- $diff = $now->diff($triggerTime);
- if ($diff->invert === 1) {
+ $recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
+ if ($reminder['recurrence_id'] >= $recurrenceId) {
+ $iterator->next();
continue;
}
- $this->backend->removeReminder($reminder['id']);
- $alarms = $this->getRemindersForVAlarm($valarm, [
- 'calendarid' => $reminder['calendar_id'],
- 'id' => $reminder['object_id'],
- ], $reminder['event_hash'], $alarmHash, true, false);
- $this->writeRemindersToDatabase($alarms);
+ foreach ($event->VALARM as $valarm) {
+ /** @var VAlarm $valarm */
+ $alarmHash = $this->getAlarmHash($valarm);
+ if ($alarmHash !== $reminder['alarm_hash']) {
+ continue;
+ }
- // Abort generating reminders after creating one successfully
- return;
- }
+ $triggerTime = $valarm->getEffectiveTriggerTime();
+
+ // If effective trigger time is in the past
+ // just skip and generate for next event
+ $diff = $now->diff($triggerTime);
+ if ($diff->invert === 1) {
+ continue;
+ }
+
+ $this->backend->removeReminder($reminder['id']);
+ $alarms = $this->getRemindersForVAlarm($valarm, [
+ 'calendarid' => $reminder['calendar_id'],
+ 'id' => $reminder['object_id'],
+ ], $calendarTimeZone, $reminder['event_hash'], $alarmHash, true, false);
+ $this->writeRemindersToDatabase($alarms);
+
+ // Abort generating reminders after creating one successfully
+ return;
+ }
- $iterator->next();
+ $iterator->next();
+ }
+ } catch (MaxInstancesExceededException $e) {
+ // Using debug logger as this isn't really an error
+ $this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]);
}
$this->backend->removeReminder($reminder['id']);
@@ -549,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));
@@ -583,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));
@@ -604,14 +623,14 @@ class ReminderService {
* @return VEvent|null
*/
private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
- int $recurrenceId,
- bool $isRecurrenceException):?VEvent {
+ int $recurrenceId,
+ bool $isRecurrenceException):?VEvent {
$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
if (count($vevents) === 0) {
return null;
}
- $uid = (string) $vevents[0]->UID;
+ $uid = (string)$vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
@@ -662,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,
@@ -724,6 +743,10 @@ class ReminderService {
if ($child->name !== 'VEVENT') {
continue;
}
+ // Ignore invalid events with no DTSTART
+ if ($child->DTSTART === null) {
+ continue;
+ }
$vevents[] = $child;
}
@@ -788,4 +811,26 @@ class ReminderService {
private function isRecurring(VEvent $vevent):bool {
return isset($vevent->RRULE) || isset($vevent->RDATE);
}
+
+ /**
+ * @param int $calendarid
+ *
+ * @return DateTimeZone
+ */
+ private function getCalendarTimeZone(int $calendarid): DateTimeZone {
+ $calendarInfo = $this->caldavBackend->getCalendarById($calendarid);
+ $tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone';
+ if (empty($calendarInfo[$tzProp])) {
+ // Defaulting to UTC
+ return new DateTimeZone('UTC');
+ }
+ // This property contains a VCALENDAR with a single VTIMEZONE
+ /** @var string $timezoneProp */
+ $timezoneProp = $calendarInfo[$tzProp];
+ /** @var VObject\Component\VCalendar $vtimezoneObj */
+ $vtimezoneObj = VObject\Reader::read($timezoneProp);
+ /** @var VObject\Component\VTimeZone $vtimezone */
+ $vtimezone = $vtimezoneObj->VTIMEZONE;
+ return $vtimezone->getTimeZone();
+ }
}
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
index aebb5a24f0e..68bb3373346 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/AbstractPrincipalBackend.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Anna Larch <anna.larch@gmx.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
@@ -33,8 +14,8 @@ use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroupManager;
-use OCP\ILogger;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
use Sabre\DAV\PropPatch;
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
use function array_intersect;
@@ -45,24 +26,6 @@ use function array_values;
abstract class AbstractPrincipalBackend implements BackendInterface {
- /** @var IDBConnection */
- private $db;
-
- /** @var IUserSession */
- private $userSession;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var ILogger */
- private $logger;
-
- /** @var ProxyMapper */
- private $proxyMapper;
-
- /** @var string */
- private $principalPrefix;
-
/** @var string */
private $dbTableName;
@@ -72,36 +35,19 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
/** @var string */
private $dbForeignKeyName;
- /** @var string */
- private $cuType;
-
- /**
- * @param IDBConnection $dbConnection
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param ILogger $logger
- * @param string $principalPrefix
- * @param string $dbPrefix
- * @param string $cuType
- */
- public function __construct(IDBConnection $dbConnection,
- IUserSession $userSession,
- IGroupManager $groupManager,
- ILogger $logger,
- ProxyMapper $proxyMapper,
- string $principalPrefix,
- string $dbPrefix,
- string $cuType) {
- $this->db = $dbConnection;
- $this->userSession = $userSession;
- $this->groupManager = $groupManager;
- $this->logger = $logger;
- $this->proxyMapper = $proxyMapper;
- $this->principalPrefix = $principalPrefix;
+ public function __construct(
+ private IDBConnection $db,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ private LoggerInterface $logger,
+ private ProxyMapper $proxyMapper,
+ private string $principalPrefix,
+ string $dbPrefix,
+ private string $cuType,
+ ) {
$this->dbTableName = 'calendar_' . $dbPrefix . 's';
$this->dbMetaDataTableName = $this->dbTableName . '_md';
$this->dbForeignKeyName = $dbPrefix . '_id';
- $this->cuType = $cuType;
}
use PrincipalProxyTrait;
@@ -140,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)) {
@@ -170,12 +116,12 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
* @return array
*/
public function getPrincipalByPath($path) {
- if (strpos($path, $this->principalPrefix) !== 0) {
+ if (!str_starts_with($path, $this->principalPrefix)) {
return null;
}
[, $name] = \Sabre\Uri\split($path);
- [$backendId, $resourceId] = explode('-', $name, 2);
+ [$backendId, $resourceId] = explode('-', $name, 2);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
@@ -319,7 +265,7 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
case IRoomMetadata::CAPACITY:
case IResourceMetadata::VEHICLE_SEATING_CAPACITY:
- $results[] = $this->searchPrincipalsByCapacity($prop,$value);
+ $results[] = $this->searchPrincipalsByCapacity($prop, $value);
break;
default:
@@ -416,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 = [];
@@ -453,7 +399,7 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
}
$usersGroups = $this->groupManager->getUserGroupIds($user);
- if (strpos($uri, 'mailto:') === 0) {
+ if (str_starts_with($uri, 'mailto:')) {
$email = substr($uri, 7);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
@@ -473,14 +419,14 @@ abstract class AbstractPrincipalBackend implements BackendInterface {
return $this->rowToPrincipal($row)['uri'];
}
- if (strpos($uri, 'principal:') === 0) {
+ if (str_starts_with($uri, 'principal:')) {
$path = substr($uri, 10);
- if (strpos($path, $this->principalPrefix) !== 0) {
+ if (!str_starts_with($path, $this->principalPrefix)) {
return null;
}
[, $name] = \Sabre\Uri\split($path);
- [$backendId, $resourceId] = explode('-', $name, 2);
+ [$backendId, $resourceId] = explode('-', $name, 2);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
@@ -525,14 +471,14 @@ 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;
}
// group restrictions contains something, but not parsable, deny access and log warning
- $json = json_decode($row['group_restrictions']);
+ $json = json_decode($row['group_restrictions'], null, 512, JSON_THROW_ON_ERROR);
if (!\is_array($json)) {
$this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource');
return false;
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
index 65203e24da5..c70d93daf52 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/ResourcePrincipalBackend.php
@@ -1,32 +1,16 @@
<?php
+
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCP\IDBConnection;
use OCP\IGroupManager;
-use OCP\ILogger;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
/**
* Class ResourcePrincipalBackend
@@ -37,18 +21,12 @@ class ResourcePrincipalBackend extends AbstractPrincipalBackend {
/**
* ResourcePrincipalBackend constructor.
- *
- * @param IDBConnection $dbConnection
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param ILogger $logger
- * @param ProxyMapper $proxyMapper
*/
public function __construct(IDBConnection $dbConnection,
- IUserSession $userSession,
- IGroupManager $groupManager,
- ILogger $logger,
- ProxyMapper $proxyMapper) {
+ IUserSession $userSession,
+ IGroupManager $groupManager,
+ LoggerInterface $logger,
+ ProxyMapper $proxyMapper) {
parent::__construct($dbConnection, $userSession, $groupManager, $logger,
$proxyMapper, 'principals/calendar-resources', 'resource', 'RESOURCE');
}
diff --git a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
index ca78ebd4bc4..5704b23ae14 100644
--- a/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
+++ b/apps/dav/lib/CalDAV/ResourceBooking/RoomPrincipalBackend.php
@@ -1,32 +1,16 @@
<?php
+
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCP\IDBConnection;
use OCP\IGroupManager;
-use OCP\ILogger;
use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
/**
* Class RoomPrincipalBackend
@@ -37,18 +21,12 @@ class RoomPrincipalBackend extends AbstractPrincipalBackend {
/**
* RoomPrincipalBackend constructor.
- *
- * @param IDBConnection $dbConnection
- * @param IUserSession $userSession
- * @param IGroupManager $groupManager
- * @param ILogger $logger
- * @param ProxyMapper $proxyMapper
*/
public function __construct(IDBConnection $dbConnection,
- IUserSession $userSession,
- IGroupManager $groupManager,
- ILogger $logger,
- ProxyMapper $proxyMapper) {
+ IUserSession $userSession,
+ IGroupManager $groupManager,
+ LoggerInterface $logger,
+ ProxyMapper $proxyMapper) {
parent::__construct($dbConnection, $userSession, $groupManager, $logger,
$proxyMapper, 'principals/calendar-rooms', 'room', 'ROOM');
}
diff --git a/apps/dav/lib/CalDAV/RetentionService.php b/apps/dav/lib/CalDAV/RetentionService.php
index 1d92d847641..399d1a46639 100644
--- a/apps/dav/lib/CalDAV/RetentionService.php
+++ b/apps/dav/lib/CalDAV/RetentionService.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
@@ -34,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 8aacc33bb46..2af6b162d8d 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
@@ -1,60 +1,34 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2017, Georg Ehrke
- * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
- * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
- *
- * @author brad2014 <brad2014@users.noreply.github.com>
- * @author Brad Rubenstein <brad@wbr.tech>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Leon Klingele <leon@struktur.de>
- * @author Nick Sweeting <git@sweeting.me>
- * @author rakekniven <mark.ziegler@rakekniven.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-FileCopyrightText: 2007-2015 fruux GmbH (https://fruux.com/)
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV\Schedule;
+use OCA\DAV\CalDAV\CalendarObject;
+use OCA\DAV\CalDAV\EventComparisonService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
-use OCP\IConfig;
-use OCP\IDBConnection;
-use OCP\IL10N;
-use OCP\ILogger;
-use OCP\IURLGenerator;
-use OCP\IUserManager;
-use OCP\L10N\IFactory as L10NFactory;
-use OCP\Mail\IEMailTemplate;
+use OCP\IAppConfig;
+use OCP\IUserSession;
use OCP\Mail\IMailer;
-use OCP\Security\ISecureRandom;
+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;
+use Sabre\DAV;
+use Sabre\DAV\INode;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
-use Sabre\VObject\DateTimeParser;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Parameter;
-use Sabre\VObject\Property;
-use Sabre\VObject\Recur\EventIterator;
+use Sabre\VObject\Reader;
/**
* iMIP handler.
@@ -72,75 +46,47 @@ use Sabre\VObject\Recur\EventIterator;
*/
class IMipPlugin extends SabreIMipPlugin {
- /** @var string */
- private $userId;
-
- /** @var IConfig */
- private $config;
-
- /** @var IMailer */
- private $mailer;
-
- /** @var ILogger */
- private $logger;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var L10NFactory */
- private $l10nFactory;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
- /** @var ISecureRandom */
- private $random;
-
- /** @var IDBConnection */
- private $db;
-
- /** @var Defaults */
- private $defaults;
-
- /** @var IUserManager */
- private $userManager;
-
+ private ?VCalendar $vCalendar = null;
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
+ 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('');
+ }
+
+ public function initialize(DAV\Server $server): void {
+ parent::initialize($server);
+ $server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
+ }
/**
- * @param IConfig $config
- * @param IMailer $mailer
- * @param ILogger $logger
- * @param ITimeFactory $timeFactory
- * @param L10NFactory $l10nFactory
- * @param IUrlGenerator $urlGenerator
- * @param Defaults $defaults
- * @param ISecureRandom $random
- * @param IDBConnection $db
- * @param string $userId
+ * Check quota before writing content
+ *
+ * @param string $uri target file URI
+ * @param INode $node Sabre Node
+ * @param resource $data data
+ * @param bool $modified modified
*/
- public function __construct(IConfig $config, IMailer $mailer, ILogger $logger,
- ITimeFactory $timeFactory, L10NFactory $l10nFactory,
- IURLGenerator $urlGenerator, Defaults $defaults,
- ISecureRandom $random, IDBConnection $db, IUserManager $userManager,
- $userId) {
- parent::__construct('');
- $this->userId = $userId;
- $this->config = $config;
- $this->mailer = $mailer;
- $this->logger = $logger;
- $this->timeFactory = $timeFactory;
- $this->l10nFactory = $l10nFactory;
- $this->urlGenerator = $urlGenerator;
- $this->random = $random;
- $this->db = $db;
- $this->defaults = $defaults;
- $this->userManager = $userManager;
+ public function beforeWriteContent($uri, INode $node, $data, $modified): void {
+ if (!$node instanceof CalendarObject) {
+ return;
+ }
+ /** @var VCalendar $vCalendar */
+ $vCalendar = Reader::read($node->get());
+ $this->setVCalendar($vCalendar);
}
/**
@@ -151,8 +97,7 @@ class IMipPlugin extends SabreIMipPlugin {
*/
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';
@@ -160,103 +105,114 @@ class IMipPlugin extends SabreIMipPlugin {
return;
}
- $summary = $iTipMessage->message->VEVENT->SUMMARY;
-
- if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto') {
- return;
- }
-
- if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
+ if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto'
+ || parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
return;
}
// don't send out mails for events that already took place
- $lastOccurrence = $this->getLastOccurrence($iTipMessage->message);
+ $lastOccurrence = $this->imipService->getLastOccurrence($iTipMessage->message);
$currentTime = $this->timeFactory->getTime();
if ($lastOccurrence < $currentTime) {
return;
}
// Strip off mailto:
- $sender = substr($iTipMessage->sender, 7);
$recipient = substr($iTipMessage->recipient, 7);
- if ($recipient === false || !$this->mailer->validateMailAddress($recipient)) {
+ if (!$this->mailer->validateMailAddress($recipient)) {
// Nothing to send if the recipient doesn't have a valid email address
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
return;
}
-
- $senderName = $iTipMessage->senderName ?: null;
- $recipientName = $iTipMessage->recipientName ?: null;
-
- if ($senderName === null || empty(trim($senderName))) {
- $user = $this->userManager->get($this->userId);
- if ($user) {
- // getDisplayName automatically uses the uid
- // if no display-name is set
- $senderName = $user->getDisplayName();
- }
+ $recipientName = $iTipMessage->recipientName ? (string)$iTipMessage->recipientName : null;
+
+ $newEvents = $iTipMessage->message;
+ $oldEvents = $this->getVCalendar();
+
+ $modified = $this->eventComparisonService->findModified($newEvents, $oldEvents);
+ /** @var VEvent $vEvent */
+ $vEvent = array_pop($modified['new']);
+ /** @var VEvent $oldVevent */
+ $oldVevent = !empty($modified['old']) && is_array($modified['old']) ? array_pop($modified['old']) : null;
+ $isModified = isset($oldVevent);
+
+ // 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)) {
+ $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;
}
- /** @var VEvent $vevent */
- $vevent = $iTipMessage->message->VEVENT;
-
- $attendee = $this->getCurrentAttendee($iTipMessage);
- $defaultLang = $this->l10nFactory->findGenericLanguage();
- $lang = $this->getAttendeeLangOrDefault($defaultLang, $attendee);
- $l10n = $this->l10nFactory->get('dav', $lang);
-
- $meetingAttendeeName = $recipientName ?: $recipient;
- $meetingInviteeName = $senderName ?: $sender;
-
- $meetingTitle = $vevent->SUMMARY;
- $meetingDescription = $vevent->DESCRIPTION;
-
-
- $meetingUrl = $vevent->URL;
- $meetingLocation = $vevent->LOCATION;
+ // we (should) have one event component left
+ // as the ITip\Broker creates one iTip message per change
+ // and triggers the "schedule" event once per message
+ // 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) {
+ $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 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';
+ return;
+ }
+ $this->imipService->setL10n($attendee);
+
+ // Build the sender name.
+ // Due to a bug in sabre, the senderName property for an iTIP message can actually also be a VObject Property
+ // If the iTIP message senderName is null or empty use the user session name as the senderName
+ if (($iTipMessage->senderName instanceof Parameter) && !empty(trim($iTipMessage->senderName->getValue()))) {
+ $senderName = trim($iTipMessage->senderName->getValue());
+ } elseif (is_string($iTipMessage->senderName) && !empty(trim($iTipMessage->senderName))) {
+ $senderName = trim($iTipMessage->senderName);
+ } elseif ($this->userSession->getUser() !== null) {
+ $senderName = trim($this->userSession->getUser()->getDisplayName());
+ } else {
+ $senderName = '';
+ }
- $defaultVal = '--';
+ $sender = substr($iTipMessage->sender, 7);
- $method = self::METHOD_REQUEST;
+ $replyingAttendee = null;
switch (strtolower($iTipMessage->method)) {
case self::METHOD_REPLY:
$method = self::METHOD_REPLY;
+ $data = $this->imipService->buildReplyBodyData($vEvent);
+ $replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage);
break;
case self::METHOD_CANCEL:
$method = self::METHOD_CANCEL;
+ $data = $this->imipService->buildCancelledBodyData($vEvent);
+ break;
+ default:
+ $method = self::METHOD_REQUEST;
+ $data = $this->imipService->buildBodyData($vEvent, $oldVevent);
break;
}
- $data = [
- 'attendee_name' => (string)$meetingAttendeeName ?: $defaultVal,
- 'invitee_name' => (string)$meetingInviteeName ?: $defaultVal,
- 'meeting_title' => (string)$meetingTitle ?: $defaultVal,
- 'meeting_description' => (string)$meetingDescription ?: $defaultVal,
- 'meeting_url' => (string)$meetingUrl ?: $defaultVal,
- ];
+ $data['attendee_name'] = ($recipientName ?: $recipient);
+ $data['invitee_name'] = ($senderName ?: $sender);
$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
- $fromName = $l10n->t('%1$s via %2$s', [$senderName, $this->defaults->getName()]);
-
- $message = $this->mailer->createMessage()
- ->setFrom([$fromEMail => $fromName])
- ->setTo([$recipient => $recipientName]);
-
- if ($sender !== false) {
- $message->setReplyTo([$sender => $senderName]);
- }
+ $fromName = $this->imipService->getFrom($senderName, $this->defaults->getName());
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
$template->addHeader();
- $summary = ((string) $summary !== '') ? (string) $summary : $l10n->t('Untitled event');
-
- $this->addSubjectAndHeading($template, $l10n, $method, $summary);
- $this->addBulletList($template, $l10n, $vevent);
+ $this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title'], $isModified, $replyingAttendee);
+ $this->imipService->addBulletList($template, $vEvent, $data);
// Only add response buttons to invitation requests: Fix Issue #11230
- if (($method == self::METHOD_REQUEST) && $this->getAttendeeRsvpOrReqForParticipant($attendee)) {
+ if (strcasecmp($method, self::METHOD_REQUEST) === 0 && $this->imipService->getAttendeeRsvpOrReqForParticipant($attendee)) {
/*
** Only offer invitation accept/reject buttons, which link back to the
@@ -277,453 +233,106 @@ class IMipPlugin extends SabreIMipPlugin {
** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
*/
- $recipientDomain = substr(strrchr($recipient, "@"), 1);
- $invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
+ $recipientDomain = substr(strrchr($recipient, '@'), 1);
+ $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)
- || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
- $this->addResponseButtons($template, $l10n, $iTipMessage, $lastOccurrence);
+ || in_array(strtolower($recipient), $invitationLinkRecipients)
+ || in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
+ $token = $this->imipService->createInvitationToken($iTipMessage, $vEvent, $lastOccurrence);
+ $this->imipService->addResponseButtons($template, $token);
+ $this->imipService->addMoreOptionsButton($template, $token);
}
}
$template->addFooter();
+ // convert iTip Message to string
+ $itip_msg = $iTipMessage->message->serialize();
- $message->useTemplate($template);
-
- $attachment = $this->mailer->createAttachment(
- $iTipMessage->message->serialize(),
- 'event.ics',// TODO(leon): Make file name unique, e.g. add event id
- 'text/calendar; method=' . $iTipMessage->method
- );
- $message->attach($attachment);
+ $mailService = null;
try {
- $failed = $this->mailer->send($message);
- $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
- if ($failed) {
- $this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
- $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
- }
- } catch (\Exception $ex) {
- $this->logger->logException($ex, ['app' => 'dav']);
- $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
- }
- }
-
- /**
- * check if event took place in the past already
- * @param VCalendar $vObject
- * @return int
- */
- private function getLastOccurrence(VCalendar $vObject) {
- /** @var VEvent $component */
- $component = $vObject->VEVENT;
-
- $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
- // Finding the last occurrence is a bit harder
- if (!isset($component->RRULE)) {
- if (isset($component->DTEND)) {
- $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
- } elseif (isset($component->DURATION)) {
- /** @var \DateTime $endDate */
- $endDate = clone $component->DTSTART->getDateTime();
- // $component->DTEND->getDateTime() returns DateTimeImmutable
- $endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
- $lastOccurrence = $endDate->getTimestamp();
- } elseif (!$component->DTSTART->hasTime()) {
- /** @var \DateTime $endDate */
- $endDate = clone $component->DTSTART->getDateTime();
- // $component->DTSTART->getDateTime() returns DateTimeImmutable
- $endDate = $endDate->modify('+1 day');
- $lastOccurrence = $endDate->getTimestamp();
- } else {
- $lastOccurrence = $firstOccurrence;
- }
- } else {
- $it = new EventIterator($vObject, (string)$component->UID);
- $maxDate = new \DateTime(self::MAX_DATE);
- if ($it->isInfinite()) {
- $lastOccurrence = $maxDate->getTimestamp();
- } else {
- $end = $it->getDtEnd();
- while ($it->valid() && $end < $maxDate) {
- $end = $it->getDtEnd();
- $it->next();
+ 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);
}
- $lastOccurrence = $end->getTimestamp();
}
- }
-
- return $lastOccurrence;
- }
- /**
- * @param Message $iTipMessage
- * @return null|Property
- */
- private function getCurrentAttendee(Message $iTipMessage) {
- /** @var VEvent $vevent */
- $vevent = $iTipMessage->message->VEVENT;
- $attendees = $vevent->select('ATTENDEE');
- foreach ($attendees as $attendee) {
- /** @var Property $attendee */
- if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
- return $attendee;
- }
- }
- return null;
- }
-
- /**
- * @param string $default
- * @param Property|null $attendee
- * @return string
- */
- private function getAttendeeLangOrDefault($default, Property $attendee = null) {
- if ($attendee !== null) {
- $lang = $attendee->offsetGet('LANGUAGE');
- if ($lang instanceof Parameter) {
- return $lang->getValue();
- }
- }
- return $default;
- }
-
- /**
- * @param Property|null $attendee
- * @return bool
- */
- private function getAttendeeRsvpOrReqForParticipant(Property $attendee = null) {
- if ($attendee !== null) {
- $rsvp = $attendee->offsetGet('RSVP');
- if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
- return true;
- }
- $role = $attendee->offsetGet('ROLE');
- // @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
- // Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
- if ($role === null
- || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
- || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
- ) {
- return true;
- }
- }
- // RFC 5545 3.2.17: default RSVP is false
- return false;
- }
-
- /**
- * @param IL10N $l10n
- * @param VEvent $vevent
- */
- private function generateWhenString(IL10N $l10n, VEvent $vevent) {
- $dtstart = $vevent->DTSTART;
- if (isset($vevent->DTEND)) {
- $dtend = $vevent->DTEND;
- } elseif (isset($vevent->DURATION)) {
- $isFloating = $vevent->DTSTART->isFloating();
- $dtend = clone $vevent->DTSTART;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
- $dtend->setDateTime($endDateTime, $isFloating);
- } elseif (!$vevent->DTSTART->hasTime()) {
- $isFloating = $vevent->DTSTART->isFloating();
- $dtend = clone $vevent->DTSTART;
- $endDateTime = $dtend->getDateTime();
- $endDateTime = $endDateTime->modify('+1 day');
- $dtend->setDateTime($endDateTime, $isFloating);
- } else {
- $dtend = clone $vevent->DTSTART;
- }
-
- $isAllDay = $dtstart instanceof Property\ICalendar\Date;
-
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
- /** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
- /** @var \DateTimeImmutable $dtstartDt */
- $dtstartDt = $dtstart->getDateTime();
- /** @var \DateTimeImmutable $dtendDt */
- $dtendDt = $dtend->getDateTime();
-
- $diff = $dtstartDt->diff($dtendDt);
-
- $dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
- $dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
-
- if ($isAllDay) {
- // One day event
- if ($diff->days === 1) {
- return $l10n->l('date', $dtstartDt, ['width' => 'medium']);
- }
-
- // 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');
-
- //event that spans over multiple days
- $localeStart = $l10n->l('date', $dtstartDt, ['width' => 'medium']);
- $localeEnd = $l10n->l('date', $dtendDt, ['width' => 'medium']);
-
- return $localeStart . ' - ' . $localeEnd;
- }
-
- /** @var Property\ICalendar\DateTime $dtstart */
- /** @var Property\ICalendar\DateTime $dtend */
- $isFloating = $dtstart->isFloating();
- $startTimezone = $endTimezone = null;
- if (!$isFloating) {
- $prop = $dtstart->offsetGet('TZID');
- if ($prop instanceof Parameter) {
- $startTimezone = $prop->getValue();
- }
-
- $prop = $dtend->offsetGet('TZID');
- if ($prop instanceof Parameter) {
- $endTimezone = $prop->getValue();
- }
- }
-
- $localeStart = $l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
- $l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
-
- // always show full date with timezone if timezones are different
- if ($startTimezone !== $endTimezone) {
- $localeEnd = $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
-
- return $localeStart . ' (' . $startTimezone . ') - ' .
- $localeEnd . ' (' . $endTimezone . ')';
- }
-
- // show only end time if date is the same
- if ($this->isDayEqual($dtstartDt, $dtendDt)) {
- $localeEnd = $l10n->l('time', $dtendDt, ['width' => 'short']);
- } else {
- $localeEnd = $l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
- $l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
- }
-
- return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
- }
-
- /**
- * @param \DateTime $dtStart
- * @param \DateTime $dtEnd
- * @return bool
- */
- private function isDayEqual(\DateTime $dtStart, \DateTime $dtEnd) {
- return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
- }
-
- /**
- * @param IEMailTemplate $template
- * @param IL10N $l10n
- * @param string $method
- * @param string $summary
- */
- private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n,
- $method, $summary) {
- if ($method === self::METHOD_CANCEL) {
- // TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
- $template->setSubject($l10n->t('Cancelled: %1$s', [$summary]));
- $template->addHeading($l10n->t('Invitation canceled'));
- } elseif ($method === self::METHOD_REPLY) {
- // TRANSLATORS Subject for email, when an invitation is updated. Ex: "Re: {{Event Name}}"
- $template->setSubject($l10n->t('Re: %1$s', [$summary]));
- $template->addHeading($l10n->t('Invitation updated'));
- } else {
- // TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
- $template->setSubject($l10n->t('Invitation: %1$s', [$summary]));
- $template->addHeading($l10n->t('Invitation'));
- }
- }
-
- /**
- * @param IEMailTemplate $template
- * @param IL10N $l10n
- * @param VEVENT $vevent
- */
- private function addBulletList(IEMailTemplate $template, IL10N $l10n, $vevent) {
- if ($vevent->SUMMARY) {
- $template->addBodyListItem($vevent->SUMMARY, $l10n->t('Title:'),
- $this->getAbsoluteImagePath('caldav/title.png'),'','',self::IMIP_INDENT);
- }
- $meetingWhen = $this->generateWhenString($l10n, $vevent);
- if ($meetingWhen) {
- $template->addBodyListItem($meetingWhen, $l10n->t('Time:'),
- $this->getAbsoluteImagePath('caldav/time.png'),'','',self::IMIP_INDENT);
- }
- if ($vevent->LOCATION) {
- $template->addBodyListItem($vevent->LOCATION, $l10n->t('Location:'),
- $this->getAbsoluteImagePath('caldav/location.png'),'','',self::IMIP_INDENT);
- }
- if ($vevent->URL) {
- $url = $vevent->URL->getValue();
- $template->addBodyListItem(sprintf('<a href="%s">%s</a>',
- htmlspecialchars($url),
- htmlspecialchars($url)),
- $l10n->t('Link:'),
- $this->getAbsoluteImagePath('caldav/link.png'),
- $url,'',self::IMIP_INDENT);
- }
-
- $this->addAttendees($template, $l10n, $vevent);
-
- /* Put description last, like an email body, since it can be arbitrarily long */
- if ($vevent->DESCRIPTION) {
- $template->addBodyListItem($vevent->DESCRIPTION->getValue(), $l10n->t('Description:'),
- $this->getAbsoluteImagePath('caldav/description.png'),'','',self::IMIP_INDENT);
- }
- }
-
- /**
- * addAttendees: add organizer and attendee names/emails to iMip mail.
- *
- * Enable with DAV setting: invitation_list_attendees (default: no)
- *
- * The default is 'no', which matches old behavior, and is privacy preserving.
- *
- * To enable including attendees in invitation emails:
- * % php occ config:app:set dav invitation_list_attendees --value yes
- *
- * @param IEMailTemplate $template
- * @param IL10N $l10n
- * @param Message $iTipMessage
- * @param int $lastOccurrence
- * @author brad2014 on github.com
- */
-
- private function addAttendees(IEMailTemplate $template, IL10N $l10n, VEvent $vevent) {
- if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
- return;
- }
-
- if (isset($vevent->ORGANIZER)) {
- /** @var Property\ICalendar\CalAddress $organizer */
- $organizer = $vevent->ORGANIZER;
- $organizerURI = $organizer->getNormalizedValue();
- [$scheme,$organizerEmail] = explode(':',$organizerURI,2); # strip off scheme mailto:
- /** @var string|null $organizerName */
- $organizerName = isset($organizer['CN']) ? $organizer['CN'] : null;
- $organizerHTML = sprintf('<a href="%s">%s</a>',
- htmlspecialchars($organizerURI),
- htmlspecialchars($organizerName ?: $organizerEmail));
- $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
- if (isset($organizer['PARTSTAT'])) {
- /** @var Parameter $partstat */
- $partstat = $organizer['PARTSTAT'];
- if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
- $organizerHTML .= ' ✔︎';
- $organizerText .= ' ✔︎';
- }
+ // 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);
}
- $template->addBodyListItem($organizerHTML, $l10n->t('Organizer:'),
- $this->getAbsoluteImagePath('caldav/organizer.png'),
- $organizerText,'',self::IMIP_INDENT);
- }
- $attendees = $vevent->select('ATTENDEE');
- if (count($attendees) === 0) {
- return;
- }
-
- $attendeesHTML = [];
- $attendeesText = [];
- foreach ($attendees as $attendee) {
- $attendeeURI = $attendee->getNormalizedValue();
- [$scheme,$attendeeEmail] = explode(':',$attendeeURI,2); # strip off scheme mailto:
- $attendeeName = isset($attendee['CN']) ? $attendee['CN'] : null;
- $attendeeHTML = sprintf('<a href="%s">%s</a>',
- htmlspecialchars($attendeeURI),
- htmlspecialchars($attendeeName ?: $attendeeEmail));
- $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
- if (isset($attendee['PARTSTAT'])
- && strcasecmp($attendee['PARTSTAT'], 'ACCEPTED') === 0) {
- $attendeeHTML .= ' ✔︎';
- $attendeeText .= ' ✔︎';
+ $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)]);
+ $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
}
- array_push($attendeesHTML, $attendeeHTML);
- array_push($attendeesText, $attendeeText);
+ } catch (\Exception $ex) {
+ $this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
+ $iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
}
-
- $template->addBodyListItem(implode('<br/>',$attendeesHTML), $l10n->t('Attendees:'),
- $this->getAbsoluteImagePath('caldav/attendees.png'),
- implode("\n",$attendeesText),'',self::IMIP_INDENT);
}
/**
- * @param IEMailTemplate $template
- * @param IL10N $l10n
- * @param Message $iTipMessage
- * @param int $lastOccurrence
+ * @return ?VCalendar
*/
- private function addResponseButtons(IEMailTemplate $template, IL10N $l10n,
- Message $iTipMessage, $lastOccurrence) {
- $token = $this->createInvitationToken($iTipMessage, $lastOccurrence);
-
- $template->addBodyButtonGroup(
- $l10n->t('Accept'),
- $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
- 'token' => $token,
- ]),
- $l10n->t('Decline'),
- $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
- 'token' => $token,
- ])
- );
-
- $moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
- 'token' => $token,
- ]);
- $html = vsprintf('<small><a href="%s">%s</a></small>', [
- $moreOptionsURL, $l10n->t('More options …')
- ]);
- $text = $l10n->t('More options at %s', [$moreOptionsURL]);
-
- $template->addBodyText($html, $text);
+ public function getVCalendar(): ?VCalendar {
+ return $this->vCalendar;
}
/**
- * @param string $path
- * @return string
+ * @param ?VCalendar $vCalendar
*/
- private function getAbsoluteImagePath($path) {
- return $this->urlGenerator->getAbsoluteURL(
- $this->urlGenerator->imagePath('core', $path)
- );
+ public function setVCalendar(?VCalendar $vCalendar): void {
+ $this->vCalendar = $vCalendar;
}
- /**
- * @param Message $iTipMessage
- * @param int $lastOccurrence
- * @return string
- */
- private function createInvitationToken(Message $iTipMessage, $lastOccurrence):string {
- $token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
-
- /** @var VEvent $vevent */
- $vevent = $iTipMessage->message->VEVENT;
- $attendee = $iTipMessage->recipient;
- $organizer = $iTipMessage->sender;
- $sequence = $iTipMessage->sequence;
- $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
- $vevent->{'RECURRENCE-ID'}->serialize() : null;
- $uid = $vevent->{'UID'};
-
- $query = $this->db->getQueryBuilder();
- $query->insert('calendar_invitations')
- ->values([
- 'token' => $query->createNamedParameter($token),
- 'attendee' => $query->createNamedParameter($attendee),
- 'organizer' => $query->createNamedParameter($organizer),
- 'sequence' => $query->createNamedParameter($sequence),
- 'recurrenceid' => $query->createNamedParameter($recurrenceId),
- 'expiration' => $query->createNamedParameter($lastOccurrence),
- 'uid' => $query->createNamedParameter($uid)
- ])
- ->execute();
-
- return $token;
- }
}
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php
new file mode 100644
index 00000000000..54c0bc31849
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php
@@ -0,0 +1,1294 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+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;
+use OCP\L10N\IFactory as L10NFactory;
+use OCP\Mail\IEMailTemplate;
+use OCP\Security\ISecureRandom;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Sabre\VObject\DateTimeParser;
+use Sabre\VObject\ITip\Message;
+use Sabre\VObject\Parameter;
+use Sabre\VObject\Property;
+use Sabre\VObject\Recur\EventIterator;
+
+class IMipService {
+
+ private IL10N $l10n;
+
+ /** @var string[] */
+ private const STRING_DIFF = [
+ 'meeting_title' => 'SUMMARY',
+ 'meeting_description' => 'DESCRIPTION',
+ 'meeting_url' => 'URL',
+ 'meeting_location' => 'LOCATION'
+ ];
+
+ 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);
+ }
+
+ /**
+ * @param string|null $senderName
+ * @param string $default
+ * @return string
+ */
+ public function getFrom(?string $senderName, string $default): string {
+ if ($senderName === null) {
+ return $default;
+ }
+
+ return $this->l10n->t('%1$s via %2$s', [$senderName, $default]);
+ }
+
+ public static function readPropertyWithDefault(VEvent $vevent, string $property, string $default) {
+ if (isset($vevent->$property)) {
+ $value = $vevent->$property->getValue();
+ if (!empty($value)) {
+ return $value;
+ }
+ }
+ return $default;
+ }
+
+ private function generateDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
+ $strikethrough = "<span style='text-decoration: line-through'>%s</span><br />%s";
+ if (!isset($vevent->$property)) {
+ return $default;
+ }
+ $newstring = $vevent->$property->getValue();
+ if (isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
+ $oldstring = $oldVEvent->$property->getValue();
+ return sprintf($strikethrough, $oldstring, $newstring);
+ }
+ return $newstring;
+ }
+
+ /**
+ * Like generateDiffString() but linkifies the property values if they are urls.
+ */
+ private function generateLinkifiedDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
+ if (!isset($vevent->$property)) {
+ return $default;
+ }
+ /** @var string|null $newString */
+ $newString = $vevent->$property->getValue();
+ $oldString = isset($oldVEvent->$property) ? $oldVEvent->$property->getValue() : null;
+ if ($oldString !== $newString) {
+ return sprintf(
+ "<span style='text-decoration: line-through'>%s</span><br />%s",
+ $this->linkify($oldString) ?? $oldString ?? '',
+ $this->linkify($newString) ?? $newString ?? ''
+ );
+ }
+ return $this->linkify($newString) ?? $newString;
+ }
+
+ /**
+ * Convert a given url to a html link element or return null otherwise.
+ */
+ private function linkify(?string $url): ?string {
+ if ($url === null) {
+ return null;
+ }
+ if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
+ return null;
+ }
+
+ return sprintf('<a href="%1$s">%1$s</a>', htmlspecialchars($url));
+ }
+
+ /**
+ * @param VEvent $vEvent
+ * @param VEvent|null $oldVEvent
+ * @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($eventReaderCurrent);
+
+ foreach (self::STRING_DIFF as $key => $property) {
+ $data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
+ }
+
+ $data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
+
+ if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
+ $data['meeting_location_html'] = $locationHtml;
+ }
+
+ 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']);
+
+ $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'] ? 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 VEvent $vEvent
+ * @return array
+ */
+ 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);
+ }
+
+ if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
+ $data['meeting_location_html'] = $locationHtml;
+ }
+
+ $data['meeting_url_html'] = $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $data['meeting_url']) : '';
+
+ // generate occurring next string
+ if ($eventReader->recurs()) {
+ $data['meeting_occurring'] = $this->generateOccurringString($eventReader);
+ }
+
+ return $data;
+ }
+
+ /**
+ * 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)
+ };
+ }
+
+ /**
+ * 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')
+ };
+ }
+
+ /**
+ * 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),
+ };
+ }
+
+ /**
+ * 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')
+ };
+
+ }
+
+ /**
+ * 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')
+ };
+
+ }
+
+ /**
+ * 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')
+ };
+ }
+
+ /**
+ * 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 {
+ $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')
+ };
+
+ }
+
+ /**
+ * @param VEvent $vEvent
+ * @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($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;
+ $newLocationHtml = $this->linkify($newLocation) ?? $newLocation;
+
+ $data = [];
+ $data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
+ $data['meeting_when'] = $newMeetingWhen;
+ $data['meeting_title_html'] = sprintf($strikethrough, $newSummary);
+ $data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event');
+ $data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : '';
+ $data['meeting_description'] = $newDescription;
+ $data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
+ $data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
+ $data['meeting_location_html'] = $newLocationHtml !== '' ? sprintf($strikethrough, $newLocationHtml) : '';
+ $data['meeting_location'] = $newLocation;
+ return $data;
+ }
+
+ /**
+ * Check if event took place in the past
+ *
+ * @param VCalendar $vObject
+ * @return int
+ */
+ public function getLastOccurrence(VCalendar $vObject) {
+ /** @var VEvent $component */
+ $component = $vObject->VEVENT;
+
+ if (isset($component->RRULE)) {
+ $it = new EventIterator($vObject, (string)$component->UID);
+ $maxDate = new \DateTime(IMipPlugin::MAX_DATE);
+ if ($it->isInfinite()) {
+ return $maxDate->getTimestamp();
+ }
+
+ $end = $it->getDtEnd();
+ while ($it->valid() && $end < $maxDate) {
+ $end = $it->getDtEnd();
+ $it->next();
+ }
+ return $end->getTimestamp();
+ }
+
+ /** @var Property\ICalendar\DateTime $dtStart */
+ $dtStart = $component->DTSTART;
+
+ if (isset($component->DTEND)) {
+ /** @var Property\ICalendar\DateTime $dtEnd */
+ $dtEnd = $component->DTEND;
+ return $dtEnd->getDateTime()->getTimeStamp();
+ }
+
+ if (isset($component->DURATION)) {
+ /** @var \DateTime $endDate */
+ $endDate = clone $dtStart->getDateTime();
+ // $component->DTEND->getDateTime() returns DateTimeImmutable
+ $endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
+ return $endDate->getTimestamp();
+ }
+
+ if (!$dtStart->hasTime()) {
+ /** @var \DateTime $endDate */
+ // $component->DTSTART->getDateTime() returns DateTimeImmutable
+ $endDate = clone $dtStart->getDateTime();
+ $endDate = $endDate->modify('+1 day');
+ return $endDate->getTimestamp();
+ }
+
+ // No computation of end time possible - return start date
+ return $dtStart->getDateTime()->getTimeStamp();
+ }
+
+ /**
+ * @param Property|null $attendee
+ */
+ public function setL10n(?Property $attendee = null) {
+ if ($attendee === null) {
+ return;
+ }
+
+ $lang = $attendee->offsetGet('LANGUAGE');
+ if ($lang instanceof Parameter) {
+ $lang = $lang->getValue();
+ $this->l10n = $this->l10nFactory->get('dav', $lang);
+ }
+ }
+
+ /**
+ * @param Property|null $attendee
+ * @return bool
+ */
+ public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
+ if ($attendee === null) {
+ return false;
+ }
+
+ $rsvp = $attendee->offsetGet('RSVP');
+ if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
+ return true;
+ }
+ $role = $attendee->offsetGet('ROLE');
+ // @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
+ // Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
+ if ($role === null
+ || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
+ || (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
+ ) {
+ return true;
+ }
+
+ // RFC 5545 3.2.17: default RSVP is false
+ return false;
+ }
+
+ /**
+ * @param IEMailTemplate $template
+ * @param string $method
+ * @param string $sender
+ * @param string $summary
+ * @param string|null $partstat
+ * @param bool $isModified
+ */
+ public function addSubjectAndHeading(IEMailTemplate $template,
+ string $method, string $sender, string $summary, bool $isModified, ?Property $replyingAttendee = null): void {
+ if ($method === IMipPlugin::METHOD_CANCEL) {
+ // TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
+ $template->setSubject($this->l10n->t('Cancelled: %1$s', [$summary]));
+ $template->addHeading($this->l10n->t('"%1$s" has been canceled', [$summary]));
+ } elseif ($method === IMipPlugin::METHOD_REPLY) {
+ // TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}"
+ $template->setSubject($this->l10n->t('Re: %1$s', [$summary]));
+ // Build the strings
+ $partstat = (isset($replyingAttendee)) ? $replyingAttendee->offsetGet('PARTSTAT') : null;
+ $partstat = ($partstat instanceof Parameter) ? $partstat->getValue() : null;
+ switch ($partstat) {
+ case 'ACCEPTED':
+ $template->addHeading($this->l10n->t('%1$s has accepted your invitation', [$sender]));
+ break;
+ case 'TENTATIVE':
+ $template->addHeading($this->l10n->t('%1$s has tentatively accepted your invitation', [$sender]));
+ break;
+ case 'DECLINED':
+ $template->addHeading($this->l10n->t('%1$s has declined your invitation', [$sender]));
+ break;
+ case null:
+ default:
+ $template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender]));
+ break;
+ }
+ } elseif ($method === IMipPlugin::METHOD_REQUEST && $isModified) {
+ // TRANSLATORS Subject for email, when an invitation is updated. Ex: "Invitation updated: {{Event Name}}"
+ $template->setSubject($this->l10n->t('Invitation updated: %1$s', [$summary]));
+ $template->addHeading($this->l10n->t('%1$s updated the event "%2$s"', [$sender, $summary]));
+ } else {
+ // TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
+ $template->setSubject($this->l10n->t('Invitation: %1$s', [$summary]));
+ $template->addHeading($this->l10n->t('%1$s would like to invite you to "%2$s"', [$sender, $summary]));
+ }
+ }
+
+ /**
+ * @param string $path
+ * @return string
+ */
+ public function getAbsoluteImagePath($path): string {
+ return $this->urlGenerator->getAbsoluteURL(
+ $this->urlGenerator->imagePath('core', $path)
+ );
+ }
+
+ /**
+ * addAttendees: add organizer and attendee names/emails to iMip mail.
+ *
+ * Enable with DAV setting: invitation_list_attendees (default: no)
+ *
+ * The default is 'no', which matches old behavior, and is privacy preserving.
+ *
+ * To enable including attendees in invitation emails:
+ * % php occ config:app:set dav invitation_list_attendees --value yes
+ *
+ * @param IEMailTemplate $template
+ * @param IL10N $this->l10n
+ * @param VEvent $vevent
+ * @author brad2014 on github.com
+ */
+ public function addAttendees(IEMailTemplate $template, VEvent $vevent) {
+ if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
+ return;
+ }
+
+ if (isset($vevent->ORGANIZER)) {
+ /** @var Property | Property\ICalendar\CalAddress $organizer */
+ $organizer = $vevent->ORGANIZER;
+ $organizerEmail = substr($organizer->getNormalizedValue(), 7);
+ /** @var string|null $organizerName */
+ $organizerName = isset($organizer->CN) ? $organizer->CN->getValue() : null;
+ $organizerHTML = sprintf('<a href="%s">%s</a>',
+ htmlspecialchars($organizer->getNormalizedValue()),
+ htmlspecialchars($organizerName ?: $organizerEmail));
+ $organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
+ if (isset($organizer['PARTSTAT'])) {
+ /** @var Parameter $partstat */
+ $partstat = $organizer['PARTSTAT'];
+ if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
+ $organizerHTML .= ' ✔︎';
+ $organizerText .= ' ✔︎';
+ }
+ }
+ $template->addBodyListItem($organizerHTML, $this->l10n->t('Organizer:'),
+ $this->getAbsoluteImagePath('caldav/organizer.png'),
+ $organizerText, '', IMipPlugin::IMIP_INDENT);
+ }
+
+ $attendees = $vevent->select('ATTENDEE');
+ if (count($attendees) === 0) {
+ return;
+ }
+
+ $attendeesHTML = [];
+ $attendeesText = [];
+ foreach ($attendees as $attendee) {
+ $attendeeEmail = substr($attendee->getNormalizedValue(), 7);
+ $attendeeName = isset($attendee['CN']) ? $attendee['CN']->getValue() : null;
+ $attendeeHTML = sprintf('<a href="%s">%s</a>',
+ htmlspecialchars($attendee->getNormalizedValue()),
+ htmlspecialchars($attendeeName ?: $attendeeEmail));
+ $attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
+ if (isset($attendee['PARTSTAT'])) {
+ /** @var Parameter $partstat */
+ $partstat = $attendee['PARTSTAT'];
+ if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
+ $attendeeHTML .= ' ✔︎';
+ $attendeeText .= ' ✔︎';
+ }
+ }
+ $attendeesHTML[] = $attendeeHTML;
+ $attendeesText[] = $attendeeText;
+ }
+
+ $template->addBodyListItem(implode('<br/>', $attendeesHTML), $this->l10n->t('Attendees:'),
+ $this->getAbsoluteImagePath('caldav/attendees.png'),
+ implode("\n", $attendeesText), '', IMipPlugin::IMIP_INDENT);
+ }
+
+ /**
+ * @param IEMailTemplate $template
+ * @param VEVENT $vevent
+ * @param $data
+ */
+ public function addBulletList(IEMailTemplate $template, VEvent $vevent, $data) {
+ $template->addBodyListItem(
+ $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('When:'),
+ $this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
+ }
+ if ($data['meeting_location'] !== '') {
+ $template->addBodyListItem($data['meeting_location_html'] ?? $data['meeting_location'], $this->l10n->t('Location:'),
+ $this->getAbsoluteImagePath('caldav/location.png'), $data['meeting_location'], '', IMipPlugin::IMIP_INDENT);
+ }
+ if ($data['meeting_url'] !== '') {
+ $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);
+
+ /* Put description last, like an email body, since it can be arbitrarily long */
+ if ($data['meeting_description']) {
+ $template->addBodyListItem($data['meeting_description_html'] ?? $data['meeting_description'], $this->l10n->t('Description:'),
+ $this->getAbsoluteImagePath('caldav/description.png'), $data['meeting_description'], '', IMipPlugin::IMIP_INDENT);
+ }
+ }
+
+ /**
+ * @param Message $iTipMessage
+ * @return null|Property
+ */
+ public function getCurrentAttendee(Message $iTipMessage): ?Property {
+ /** @var VEvent $vevent */
+ $vevent = $iTipMessage->message->VEVENT;
+ $attendees = $vevent->select('ATTENDEE');
+ foreach ($attendees as $attendee) {
+ if ($iTipMessage->method === 'REPLY' && strcasecmp($attendee->getValue(), $iTipMessage->sender) === 0) {
+ /** @var Property $attendee */
+ return $attendee;
+ } elseif (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
+ /** @var Property $attendee */
+ return $attendee;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param Message $iTipMessage
+ * @param VEvent $vevent
+ * @param int $lastOccurrence
+ * @return string
+ */
+ public function createInvitationToken(Message $iTipMessage, VEvent $vevent, int $lastOccurrence): string {
+ $token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
+
+ $attendee = $iTipMessage->recipient;
+ $organizer = $iTipMessage->sender;
+ $sequence = $iTipMessage->sequence;
+ $recurrenceId = isset($vevent->{'RECURRENCE-ID'})
+ ? $vevent->{'RECURRENCE-ID'}->serialize() : null;
+ $uid = $vevent->{'UID'}?->getValue();
+
+ $query = $this->db->getQueryBuilder();
+ $query->insert('calendar_invitations')
+ ->values([
+ 'token' => $query->createNamedParameter($token),
+ 'attendee' => $query->createNamedParameter($attendee),
+ 'organizer' => $query->createNamedParameter($organizer),
+ 'sequence' => $query->createNamedParameter($sequence),
+ 'recurrenceid' => $query->createNamedParameter($recurrenceId),
+ 'expiration' => $query->createNamedParameter($lastOccurrence),
+ 'uid' => $query->createNamedParameter($uid)
+ ])
+ ->executeStatement();
+
+ return $token;
+ }
+
+ /**
+ * @param IEMailTemplate $template
+ * @param $token
+ */
+ public function addResponseButtons(IEMailTemplate $template, $token) {
+ $template->addBodyButtonGroup(
+ $this->l10n->t('Accept'),
+ $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
+ 'token' => $token,
+ ]),
+ $this->l10n->t('Decline'),
+ $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
+ 'token' => $token,
+ ])
+ );
+ }
+
+ public function addMoreOptionsButton(IEMailTemplate $template, $token) {
+ $moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
+ 'token' => $token,
+ ]);
+ $html = vsprintf('<small><a href="%s">%s</a></small>', [
+ $moreOptionsURL, $this->l10n->t('More options …')
+ ]);
+ $text = $this->l10n->t('More options at %s', [$moreOptionsURL]);
+
+ $template->addBodyText($html, $text);
+ }
+
+ public function getReplyingAttendee(Message $iTipMessage): ?Property {
+ /** @var VEvent $vevent */
+ $vevent = $iTipMessage->message->VEVENT;
+ $attendees = $vevent->select('ATTENDEE');
+ foreach ($attendees as $attendee) {
+ /** @var Property $attendee */
+ if (strcasecmp($attendee->getValue(), $iTipMessage->sender) === 0) {
+ return $attendee;
+ }
+ }
+ return null;
+ }
+
+ public function isRoomOrResource(Property $attendee): bool {
+ $cuType = $attendee->offsetGet('CUTYPE');
+ if (!$cuType instanceof Parameter) {
+ return false;
+ }
+ $type = $cuType->getValue() ?? 'INDIVIDUAL';
+ if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM'], true)) {
+ // Don't send emails to things
+ return true;
+ }
+ 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 96bacce4454..a001df8b2a8 100644
--- a/apps/dav/lib/CalDAV/Schedule/Plugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php
@@ -1,43 +1,30 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
- * @copyright Copyright (c) 2016, Joas Schilling <coding@schilljs.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Daniel Kesselberg <mail@danielkesselberg.de>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Schedule;
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;
use Sabre\DAV\Server;
use Sabre\DAV\Xml\Property\LocalHref;
+use Sabre\DAVACL\IACL;
use Sabre\DAVACL\IPrincipal;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
@@ -47,18 +34,14 @@ use Sabre\VObject\Component\VEvent;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\FreeBusyGenerator;
use Sabre\VObject\ITip;
+use Sabre\VObject\ITip\SameOrganizerForAllComponentsException;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
-use function \Sabre\Uri\split;
+use function Sabre\Uri\split;
class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
- /**
- * @var IConfig
- */
- private $config;
-
/** @var ITip\Message[] */
private $schedulingResponses = [];
@@ -71,8 +54,11 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
/**
* @param IConfig $config
*/
- public function __construct(IConfig $config) {
- $this->config = $config;
+ public function __construct(
+ private IConfig $config,
+ private LoggerInterface $logger,
+ private DefaultCalendarValidator $defaultCalendarValidator,
+ ) {
}
/**
@@ -86,6 +72,20 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
$server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
$server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
+
+ // We allow mutating the default calendar URL through the CustomPropertiesBackend
+ // (oc_properties table)
+ $server->protectedProperties = array_filter(
+ $server->protectedProperties,
+ static fn (string $property) => $property !== self::SCHEDULE_DEFAULT_CALENDAR_URL,
+ );
+ }
+
+ /**
+ * Returns an instance of the iTip\Broker.
+ */
+ protected function createITipBroker(): TipBroker {
+ return new TipBroker();
}
/**
@@ -139,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;
}
@@ -156,20 +161,91 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$this->pathOfCalendarObjectChange = $request->getPath();
}
- parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew);
+ try {
+
+ // 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);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function beforeUnbind($path): void {
+ try {
+ parent::beforeUnbind($path);
+ } catch (SameOrganizerForAllComponentsException $e) {
+ $node = $this->server->tree->getNodeForPath($path);
+ if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
+ throw $e;
+ }
+
+ /** @var VCalendar $vCal */
+ $vCal = Reader::read($node->get());
+ $this->handleSameOrganizerException($e, $vCal, $path);
+ }
}
/**
* @inheritDoc
*/
public function scheduleLocalDelivery(ITip\Message $iTipMessage):void {
- parent::scheduleLocalDelivery($iTipMessage);
+ /** @var VEvent|null $vevent */
+ $vevent = $iTipMessage->message->VEVENT ?? null;
+
+ // Strip VALARMs from incoming VEVENT
+ if ($vevent && isset($vevent->VALARM)) {
+ $vevent->remove('VALARM');
+ }
+ parent::scheduleLocalDelivery($iTipMessage);
// We only care when the message was successfully delivered locally
+ // Log all possible codes returned from the parent method that mean something went wrong
+ // 3.7, 3.8, 5.0, 5.2
if ($iTipMessage->scheduleStatus !== '1.2;Message delivered locally') {
+ $this->logger->debug('Message not delivered locally with status: ' . $iTipMessage->scheduleStatus);
return;
}
-
// We only care about request. reply and cancel are properly handled
// by parent::scheduleLocalDelivery already
if (strcasecmp($iTipMessage->method, 'REQUEST') !== 0) {
@@ -178,41 +254,38 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
// If parent::scheduleLocalDelivery set scheduleStatus to 1.2,
// it means that it was successfully delivered locally.
- // Meaning that the ACL plugin is loaded and that a principial
+ // Meaning that the ACL plugin is loaded and that a principal
// exists for the given recipient id, no need to double check
/** @var \Sabre\DAVACL\Plugin $aclPlugin */
$aclPlugin = $this->server->getPlugin('acl');
$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 neither room nor resource, not processing further');
return;
}
$attendee = $this->getCurrentAttendee($iTipMessage);
if (!$attendee) {
+ $this->logger->debug('No attendee set for scheduling message');
return;
}
// We only respond when a response was actually requested
$rsvp = $this->getAttendeeRSVP($attendee);
if (!$rsvp) {
+ $this->logger->debug('No RSVP requested for attendee ' . $attendee->getValue());
return;
}
- if (!isset($iTipMessage->message)) {
- return;
- }
-
- $vcalendar = $iTipMessage->message;
- if (!isset($vcalendar->VEVENT)) {
+ if (!$vevent) {
+ $this->logger->debug('No VEVENT set to process on scheduling message');
return;
}
- /** @var Component $vevent */
- $vevent = $vcalendar->VEVENT;
-
// We don't support autoresponses for recurrencing events for now
if (isset($vevent->RRULE) || isset($vevent->RDATE)) {
+ $this->logger->debug('VEVENT is a recurring event, autoresponding not supported');
return;
}
@@ -299,12 +372,14 @@ EOF;
return null;
}
- if (strpos($principalUrl, 'principals/users') === 0) {
+ $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);
$uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
$displayName = CalDavBackend::PERSONAL_CALENDAR_NAME;
- } elseif (strpos($principalUrl, 'principals/calendar-resources') === 0 ||
- strpos($principalUrl, 'principals/calendar-rooms') === 0) {
+ } elseif ($isResourceOrRoom) {
$uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI;
$displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME;
} else {
@@ -315,10 +390,65 @@ EOF;
/** @var CalendarHome $calendarHome */
$calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
- if (!$calendarHome->childExists($uri)) {
- $calendarHome->getCalDAVBackend()->createCalendar($principalUrl, $uri, [
- '{DAV:}displayname' => $displayName,
- ]);
+ $currentCalendarDeleted = false;
+ if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) {
+ // If the default calendar doesn't exist
+ if ($isResourceOrRoom) {
+ // Resources or rooms can't be in the trashbin, so we're fine
+ $this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
+ } else {
+ // And we're not handling scheduling on resource/room booking
+ $userCalendars = [];
+ /**
+ * If the default calendar of the user isn't set and the
+ * fallback doesn't match any of the user's calendar
+ * try to find the first "personal" calendar we can write to
+ * instead of creating a new one.
+ * A appropriate personal calendar to receive invites:
+ * - 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)) {
+ continue;
+ }
+
+ try {
+ $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
+ } catch (DavException $e) {
+ continue;
+ }
+
+ $userCalendars[] = $node;
+ }
+
+ if (count($userCalendars) > 0) {
+ // Calendar backend returns calendar by calendarorder property
+ $uri = $userCalendars[0]->getName();
+ } else {
+ // Otherwise if we have really nothing, create a new calendar
+ if ($currentCalendarDeleted) {
+ // 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) {
+ $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);
+ }
+ }
}
$result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1);
@@ -373,7 +503,7 @@ EOF;
* @param Property|null $attendee
* @return bool
*/
- private function getAttendeeRSVP(Property $attendee = null):bool {
+ private function getAttendeeRSVP(?Property $attendee = null):bool {
if ($attendee !== null) {
$rsvp = $attendee->offsetGet('RSVP');
if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
@@ -448,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;
}
@@ -533,7 +665,7 @@ EOF;
}
// If more than one Free-Busy property was returned, it means that an event
- // starts or ends inside this time-range, so it's not availabe and we return false
+ // starts or ends inside this time-range, so it's not available and we return false
if (count($freeBusyProperties) > 1) {
return false;
}
@@ -564,4 +696,63 @@ EOF;
return $email;
}
+
+ private function getCalendar(CalendarHome $calendarHome, string $uri): INode {
+ return $calendarHome->getChild($uri);
+ }
+
+ private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool {
+ $calendar = $this->getCalendar($calendarHome, $uri);
+ return $calendar instanceof Calendar && $calendar->isDeleted();
+ }
+
+ private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void {
+ $calendarHome->getCalDAVBackend()->createCalendar($principalUri, $uri, [
+ '{DAV:}displayname' => $displayName,
+ ]);
+ }
+
+ 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.
+ *
+ * @throws SameOrganizerForAllComponentsException If the exception should not be ignored
+ */
+ private function handleSameOrganizerException(
+ SameOrganizerForAllComponentsException $e,
+ VCalendar $vCal,
+ string $calendarPath,
+ ): void {
+ // This is very hacky! However, we want to allow saving events with multiple
+ // organizers. Those events are not RFC compliant, but sometimes imported from major
+ // external calendar services (e.g. Google). If the current user is not an organizer of
+ // the event we ignore the exception as no scheduling messages will be sent anyway.
+
+ // It would be cleaner to patch Sabre to validate organizers *after* checking if
+ // scheduling messages are necessary. Currently, organizers are validated first and
+ // afterwards the broker checks if messages should be scheduled. So the code will throw
+ // even if the organizers are not relevant. This is to ensure compliance with RFCs but
+ // a bit too strict for real world usage.
+
+ if (!isset($vCal->VEVENT)) {
+ throw $e;
+ }
+
+ $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
+ if (!($calendarNode instanceof IACL)) {
+ // Should always be an instance of IACL but just to be sure
+ throw $e;
+ }
+
+ $addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
+ foreach ($vCal->VEVENT as $vevent) {
+ if (in_array($vevent->ORGANIZER->getNormalizedValue(), $addresses, true)) {
+ // User is an organizer => throw the exception
+ throw $e;
+ }
+ }
+ }
}
diff --git a/apps/dav/lib/CalDAV/Search/SearchPlugin.php b/apps/dav/lib/CalDAV/Search/SearchPlugin.php
index d08a5749ab2..27e39a76305 100644
--- a/apps/dav/lib/CalDAV/Search/SearchPlugin.php
+++ b/apps/dav/lib/CalDAV/Search/SearchPlugin.php
@@ -1,33 +1,14 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
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;
@@ -81,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;
}
/**
@@ -129,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) {
@@ -154,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 d5b7c834e36..21a4fff1caf 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/CompFilter.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
index 2c435ba3650..a98b325397b 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/LimitFilter.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
index a6f41d09161..ef438aa0258 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/OffsetFilter.php
@@ -1,27 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
index c25450a0c94..0c31f32348a 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/ParamFilter.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
index 990b0ebf730..251120e35cc 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/PropFilter.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
index 06fe39a463b..6d6bf958496 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
diff --git a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
index 98efe36ee43..6ece88fa87b 100644
--- a/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
+++ b/apps/dav/lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php
@@ -1,26 +1,8 @@
<?php
+
/**
- * @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Search\Xml\Request;
diff --git a/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
new file mode 100644
index 00000000000..311157994e2
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV\Security;
+
+use OC\Security\RateLimiting\Exception\RateLimitExceededException;
+use OC\Security\RateLimiting\Limiter;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
+use OCP\IAppConfig;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+use Sabre\DAV;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\ServerPlugin;
+use function count;
+use function explode;
+
+class RateLimitingPlugin extends ServerPlugin {
+
+ private Limiter $limiter;
+
+ public function __construct(
+ Limiter $limiter,
+ private IUserManager $userManager,
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ private IAppConfig $config,
+ private ?string $userId,
+ ) {
+ $this->limiter = $limiter;
+ }
+
+ public function initialize(DAV\Server $server): void {
+ $server->on('beforeBind', [$this, 'beforeBind'], 1);
+ }
+
+ public function beforeBind(string $path): void {
+ if ($this->userId === null) {
+ // We only care about authenticated users here
+ return;
+ }
+ $user = $this->userManager->get($this->userId);
+ if ($user === null) {
+ // We only care about authenticated users here
+ return;
+ }
+
+ $pathParts = explode('/', $path);
+ if (count($pathParts) === 3 && $pathParts[0] === 'calendars') {
+ // Path looks like calendars/username/calendarname so a new calendar or subscription is created
+ try {
+ $this->limiter->registerUserRequest(
+ 'caldav-create-calendar',
+ $this->config->getValueInt('dav', 'rateLimitCalendarCreation', 10),
+ $this->config->getValueInt('dav', 'rateLimitPeriodCalendarCreation', 3600),
+ $user
+ );
+ } catch (RateLimitExceededException $e) {
+ throw new TooManyRequests('Too many calendars created', 0, $e);
+ }
+
+ $calendarLimit = $this->config->getValueInt('dav', 'maximumCalendarsSubscriptions', 30);
+ if ($calendarLimit === -1) {
+ return;
+ }
+ $numCalendars = $this->calDavBackend->getCalendarsForUserCount('principals/users/' . $user->getUID());
+ $numSubscriptions = $this->calDavBackend->getSubscriptionsForUserCount('principals/users/' . $user->getUID());
+
+ if (($numCalendars + $numSubscriptions) >= $calendarLimit) {
+ $this->logger->warning('Maximum number of calendars/subscriptions reached', [
+ 'calendars' => $numCalendars,
+ 'subscription' => $numSubscriptions,
+ 'limit' => $calendarLimit,
+ ]);
+ throw new Forbidden('Calendar limit reached', 0);
+ }
+ }
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Sharing/Backend.php b/apps/dav/lib/CalDAV/Sharing/Backend.php
new file mode 100644
index 00000000000..fc5d65b5994
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Sharing/Backend.php
@@ -0,0 +1,30 @@
+<?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\Sharing;
+
+use OCA\DAV\Connector\Sabre\Principal;
+use OCA\DAV\DAV\Sharing\Backend as SharingBackend;
+use OCP\ICacheFactory;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+
+class Backend extends SharingBackend {
+
+ public function __construct(
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ private Principal $principalBackend,
+ private ICacheFactory $cacheFactory,
+ private Service $service,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct($this->userManager, $this->groupManager, $this->principalBackend, $this->cacheFactory, $this->service, $this->logger);
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Sharing/Service.php b/apps/dav/lib/CalDAV/Sharing/Service.php
new file mode 100644
index 00000000000..4f0554f09bd
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Sharing/Service.php
@@ -0,0 +1,21 @@
+<?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\Sharing;
+
+use OCA\DAV\DAV\Sharing\SharingMapper;
+use OCA\DAV\DAV\Sharing\SharingService;
+
+class Service extends SharingService {
+ protected string $resourceType = 'calendar';
+ 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
new file mode 100644
index 00000000000..9ee0e9bf356
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Status/StatusService.php
@@ -0,0 +1,186 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\CalDAV\Status;
+
+use DateTimeImmutable;
+use OC\Calendar\CalendarQuery;
+use OCA\DAV\CalDAV\CalendarImpl;
+use OCA\UserStatus\Service\StatusService as UserStatusService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Calendar\IManager;
+use OCP\DB\Exception;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IUser as User;
+use OCP\IUserManager;
+use OCP\User\IAvailabilityCoordinator;
+use OCP\UserStatus\IUserStatus;
+use Psr\Log\LoggerInterface;
+use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
+
+class StatusService {
+ private ICache $cache;
+ public function __construct(
+ private ITimeFactory $timeFactory,
+ private IManager $calendarManager,
+ private IUserManager $userManager,
+ private UserStatusService $userStatusService,
+ private IAvailabilityCoordinator $availabilityCoordinator,
+ private ICacheFactory $cacheFactory,
+ private LoggerInterface $logger,
+ ) {
+ $this->cache = $cacheFactory->createLocal('CalendarStatusService');
+ }
+
+ public function processCalendarStatus(string $userId): void {
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ return;
+ }
+
+ $availability = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
+ 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) {
+ $calendarEvents = $this->getCalendarEvents($user);
+ $this->cache->set($userId, $calendarEvents, 300);
+ }
+
+ if (empty($calendarEvents)) {
+ try {
+ $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ } catch (Exception $e) {
+ if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ // A different process might have written another status
+ // update to the DB while we're processing our stuff.
+ // We cannot safely restore the status as we don't know which one is valid at this point
+ // So let's silently log this one and exit
+ $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]);
+ return;
+ }
+ }
+ $this->logger->debug('No calendar events found for status check', ['user' => $userId]);
+ return;
+ }
+
+ try {
+ $currentStatus = $this->userStatusService->findByUserId($userId);
+ // Was the status set by anything other than the calendar automation?
+ $userStatusTimestamp = $currentStatus->getIsUserDefined() && $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY ? $currentStatus->getStatusTimestamp() : null;
+ } catch (DoesNotExistException) {
+ $userStatusTimestamp = null;
+ $currentStatus = null;
+ }
+
+ 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;
+ }
+
+ // Filter events to see if we have any that apply to the calendar status
+ $applicableEvents = array_filter($calendarEvents, static function (array $calendarEvent) use ($userStatusTimestamp): bool {
+ if (empty($calendarEvent['objects'])) {
+ return false;
+ }
+ $component = $calendarEvent['objects'][0];
+ if (isset($component['X-NEXTCLOUD-OUT-OF-OFFICE'])) {
+ return false;
+ }
+ if (isset($component['DTSTART']) && $userStatusTimestamp !== null) {
+ /** @var DateTimeImmutable $dateTime */
+ $dateTime = $component['DTSTART'][0];
+ if ($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) {
+ return false;
+ }
+ }
+ // Ignore events that are transparent
+ if (isset($component['TRANSP']) && strcasecmp($component['TRANSP'][0], 'TRANSPARENT') === 0) {
+ return false;
+ }
+ return true;
+ });
+
+ if (empty($applicableEvents)) {
+ try {
+ $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ } catch (Exception $e) {
+ if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ // A different process might have written another status
+ // update to the DB while we're processing our stuff.
+ // We cannot safely restore the status as we don't know which one is valid at this point
+ // So let's silently log this one and exit
+ $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]);
+ return;
+ }
+ }
+ $this->logger->debug('No status relevant events found, skipping calendar status change', ['user' => $userId]);
+ return;
+ }
+
+ // Only update the status if it's neccesary otherwise we mess up the timestamp
+ 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
+ // 3. Event is not set to be transparent
+ $count = count($applicableEvents);
+ $this->logger->debug("Found $count applicable event(s), changing user status", ['user' => $userId]);
+ $this->userStatusService->setUserStatus(
+ $userId,
+ IUserStatus::BUSY,
+ IUserStatus::MESSAGE_CALENDAR_BUSY,
+ true
+ );
+ }
+ }
+
+ private function getCalendarEvents(User $user): array {
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $user->getUID());
+ if (empty($calendars)) {
+ return [];
+ }
+
+ $query = $this->calendarManager->newQuery('principals/users/' . $user->getUID());
+ foreach ($calendars as $calendarObject) {
+ // We can only work with a calendar if it exposes its scheduling information
+ if (!$calendarObject instanceof CalendarImpl) {
+ continue;
+ }
+
+ $sct = $calendarObject->getSchedulingTransparency();
+ if ($sct !== null && strtolower($sct->getValue()) == ScheduleCalendarTransp::TRANSPARENT) {
+ // If a calendar is marked as 'transparent', it means we must
+ // ignore it for free-busy purposes.
+ continue;
+ }
+ $query->addSearchCalendar($calendarObject->getUri());
+ }
+
+ $dtStart = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime());
+ $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())) {
+ // Query the next hour
+ $query->setTimerangeStart($dtStart);
+ $query->setTimerangeEnd($dtEnd);
+ return $this->calendarManager->searchForPrincipal($query);
+ }
+
+ return [];
+ }
+}
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
new file mode 100644
index 00000000000..a7709bde0f9
--- /dev/null
+++ b/apps/dav/lib/CalDAV/TimezoneService.php
@@ -0,0 +1,93 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use OCA\DAV\Db\PropertyMapper;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\IManager;
+use OCP\IConfig;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VTimeZone;
+use Sabre\VObject\Reader;
+use function array_reduce;
+
+class TimezoneService {
+
+ public function __construct(
+ private IConfig $config,
+ private PropertyMapper $propertyMapper,
+ private IManager $calendarManager,
+ ) {
+ }
+
+ public function getUserTimezone(string $userId): ?string {
+ $fromConfig = $this->config->getUserValue(
+ $userId,
+ 'core',
+ 'timezone',
+ );
+ if ($fromConfig !== '') {
+ return $fromConfig;
+ }
+
+ $availabilityPropPath = 'calendars/' . $userId . '/inbox';
+ $availabilityProp = '{' . Plugin::NS_CALDAV . '}calendar-availability';
+ $availabilities = $this->propertyMapper->findPropertyByPathAndName($userId, $availabilityPropPath, $availabilityProp);
+ if (!empty($availabilities)) {
+ $availability = $availabilities[0]->getPropertyvalue();
+ /** @var VCalendar $vCalendar */
+ $vCalendar = Reader::read($availability);
+ /** @var VTimeZone $vTimezone */
+ $vTimezone = $vCalendar->VTIMEZONE;
+ // Sabre has a fallback to date_default_timezone_get
+ return $vTimezone->getTimeZone()->getName();
+ }
+
+ $principal = 'principals/users/' . $userId;
+ $uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
+ $calendars = $this->calendarManager->getCalendarsForPrincipal($principal);
+
+ /** @var ?VTimeZone $personalCalendarTimezone */
+ $personalCalendarTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) use ($uri) {
+ if ($acc !== null) {
+ return $acc;
+ }
+ if ($calendar->getUri() === $uri && !$calendar->isDeleted() && $calendar instanceof CalendarImpl) {
+ return $calendar->getSchedulingTimezone();
+ }
+ return null;
+ });
+ if ($personalCalendarTimezone !== null) {
+ return $personalCalendarTimezone->getTimeZone()->getName();
+ }
+
+ // No timezone in the personalCalendarTimezone calendar or no personalCalendarTimezone calendar
+ // Loop through all calendars until we find a timezone.
+ /** @var ?VTimeZone $firstTimezone */
+ $firstTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) {
+ if ($acc !== null) {
+ return $acc;
+ }
+ if (!$calendar->isDeleted() && $calendar instanceof CalendarImpl) {
+ return $calendar->getSchedulingTimezone();
+ }
+ return null;
+ });
+ if ($firstTimezone !== null) {
+ return $firstTimezone->getTimeZone()->getName();
+ }
+ return null;
+ }
+
+ public function getDefaultTimezone(): string {
+ return $this->config->getSystemValueString('default_timezone', 'UTC');
+ }
+
+}
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 5730b7a1002..d8c429f2056 100644
--- a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
+++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
@@ -35,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() {
@@ -89,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;
@@ -100,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 {
@@ -108,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 20d05c047b1..f75e19689f1 100644
--- a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
+++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
@@ -31,23 +14,22 @@ use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\NotImplemented;
+use Sabre\DAVACL\ACLTrait;
+use Sabre\DAVACL\IACL;
use function array_map;
use function implode;
use function preg_match;
-class DeletedCalendarObjectsCollection implements ICalendarObjectContainer {
- public const NAME = 'objects';
-
- /** @var CalDavBackend */
- protected $caldavBackend;
+class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL {
+ use ACLTrait;
- /** @var mixed[] */
- private $principalInfo;
+ public const NAME = 'objects';
- public function __construct(CalDavBackend $caldavBackend,
- array $principalInfo) {
- $this->caldavBackend = $caldavBackend;
- $this->principalInfo = $principalInfo;
+ public function __construct(
+ protected CalDavBackend $caldavBackend,
+ /** @var mixed[] */
+ private array $principalInfo,
+ ) {
}
/**
@@ -64,7 +46,7 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer {
$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
@@ -129,4 +111,23 @@ class DeletedCalendarObjectsCollection implements ICalendarObjectContainer {
[$calendarInfo['id'], 'ics'],
);
}
+
+ public function getOwner() {
+ return $this->principalInfo['uri'];
+ }
+
+ public function getACL(): array {
+ return [
+ [
+ 'privilege' => '{DAV:}read',
+ 'principal' => $this->getOwner(),
+ 'protected' => true,
+ ],
+ [
+ 'privilege' => '{DAV:}unbind',
+ 'principal' => '{DAV:}owner',
+ 'protected' => true,
+ ]
+ ];
+ }
}
diff --git a/apps/dav/lib/CalDAV/Trashbin/Plugin.php b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
index 58ff76beca1..6f58b1f3110 100644
--- a/apps/dav/lib/CalDAV/Trashbin/Plugin.php
+++ b/apps/dav/lib/CalDAV/Trashbin/Plugin.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
@@ -49,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/RestoreTarget.php b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php
index 31331957c49..6641148de2b 100644
--- a/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php
+++ b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
diff --git a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
index 34d11e51eb3..1c76bd2295d 100644
--- a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
+++ b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php
@@ -3,25 +3,8 @@
declare(strict_types=1);
/**
- * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Trashbin;
@@ -43,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 3dd8a7c81e5..e07be39c7b4 100644
--- a/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php
+++ b/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php
@@ -3,30 +3,12 @@
declare(strict_types=1);
/**
- * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\WebcalCaching;
-use OCA\DAV\CalDAV\CalendarHome;
+use OCA\DAV\CalDAV\CalendarRoot;
use OCP\IRequest;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Server;
@@ -41,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/'
];
/**
@@ -71,6 +57,11 @@ class Plugin extends ServerPlugin {
if ($magicHeader === 'On') {
$this->enabled = true;
}
+
+ $isExportRequest = $request->getMethod() === 'GET' && array_key_exists('export', $request->getParams());
+ if ($isExportRequest) {
+ $this->enabled = true;
+ }
}
/**
@@ -85,7 +76,7 @@ class Plugin extends ServerPlugin {
*/
public function initialize(Server $server) {
$this->server = $server;
- $server->on('beforeMethod:*', [$this, 'beforeMethod']);
+ $server->on('beforeMethod:*', [$this, 'beforeMethod'], 15);
}
/**
@@ -98,21 +89,20 @@ class Plugin extends ServerPlugin {
}
$path = $request->getPath();
+ if (!str_starts_with($path, 'calendars/')) {
+ return;
+ }
+
$pathParts = explode('/', ltrim($path, '/'));
if (\count($pathParts) < 2) {
return;
}
- // $calendarHomePath will look like: calendars/username
- $calendarHomePath = $pathParts[0] . '/' . $pathParts[1];
try {
- $calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
- if (!($calendarHome instanceof CalendarHome)) {
- //how did we end up here?
- return;
+ $calendarRoot = $this->server->tree->getNodeForPath($pathParts[0]);
+ if ($calendarRoot instanceof CalendarRoot) {
+ $calendarRoot->enableReturnCachedSubscriptions($pathParts[1]);
}
-
- $calendarHome->enableCachedSubscriptionsForThisRequest();
} catch (NotFound $ex) {
return;
}
diff --git a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
index 543d15e0179..a0981e6dec1 100644
--- a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
+++ b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php
@@ -3,94 +3,42 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2020, Thomas Citharel <nextcloud@tcit.fr>
- * @copyright Copyright (c) 2020, leith abdulla (<online-nextcloud@eleith.com>)
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author eleith <online+github@eleith.com>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- * @author Thomas Citharel <nextcloud@tcit.fr>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
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 OCP\ILogger;
-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;
-use Sabre\VObject\Recur\NoInstancesException;
use Sabre\VObject\ParseException;
use Sabre\VObject\Reader;
+use Sabre\VObject\Recur\NoInstancesException;
use Sabre\VObject\Splitter\ICalendar;
use Sabre\VObject\UUIDUtil;
use function count;
class RefreshWebcalService {
- /** @var CalDavBackend */
- private $calDavBackend;
-
- /** @var IClientService */
- private $clientService;
-
- /** @var IConfig */
- private $config;
-
- /** @var ILogger */
- private $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';
- /**
- * RefreshWebcalJob constructor.
- *
- * @param CalDavBackend $calDavBackend
- * @param IClientService $clientService
- * @param IConfig $config
- * @param ILogger $logger
- */
- public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, ILogger $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,
+ ) {
}
- /**
- * @param string $principalUri
- * @param string $uri
- */
public function refreshSubscription(string $principalUri, string $uri) {
$subscription = $this->getSubscription($principalUri, $uri);
$mutations = [];
@@ -98,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;
@@ -110,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') {
@@ -132,19 +90,66 @@ class RefreshWebcalService {
if ($stripAttachments) {
unset($component->{'ATTACH'});
}
+
+ $uid = $component->{ 'UID' }->getValue();
}
if ($stripTodos && $compName === 'VTODO') {
continue;
}
- $uri = $this->getRandomCalendarObjectUri();
- $calendarData = $vObject->serialize();
+ if (!isset($uid)) {
+ continue;
+ }
+
try {
- $this->calDavBackend->createCalendarObject($subscription['id'], $uri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
- } catch (NoInstancesException | BadRequest $ex) {
- $this->logger->logException($ex);
+ $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);
@@ -154,21 +159,14 @@ class RefreshWebcalService {
$this->updateSubscription($subscription, $mutations);
} catch (ParseException $ex) {
- $subscriptionId = $subscription['id'];
-
- $this->logger->logException($ex);
- $this->logger->warning("Subscription $subscriptionId could not be refreshed due to a parsing error");
+ $this->logger->error('Subscription {subscriptionId} could not be refreshed due to a parsing error', ['exception' => $ex, 'subscriptionId' => $subscription['id']]);
}
}
/**
* loads subscription from backend
- *
- * @param string $principalUri
- * @param string $uri
- * @return array|null
*/
- public function getSubscription(string $principalUri, string $uri) {
+ public function getSubscription(string $principalUri, string $uri): ?array {
$subscriptions = array_values(array_filter(
$this->calDavBackend->getSubscriptionsForUser($principalUri),
function ($sub) use ($uri) {
@@ -183,117 +181,6 @@ class RefreshWebcalService {
return $subscriptions[0];
}
- /**
- * gets webcal feed from remote server
- *
- * @param array $subscription
- * @param array &$mutations
- * @return null|string
- */
- private function queryWebcalFeed(array $subscription, array &$mutations) {
- $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 Crawler');
- }));
- $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->debug("Subscription $subscriptionId could not be parsed");
- 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->debug("Subscription $subscriptionId could not be parsed");
- 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->debug("Subscription $subscriptionId could not be parsed");
- return null;
- }
- return $vCalendar->serialize();
- }
- } catch (LocalServerException $ex) {
- $this->logger->logException($ex, [
- 'message' => "Subscription $subscriptionId was not refreshed because it violates local access rules",
- 'level' => ILogger::WARN,
- ]);
-
- return null;
- } catch (Exception $ex) {
- $this->logger->logException($ex, [
- 'message' => "Subscription $subscriptionId could not be refreshed due to a network error",
- 'level' => ILogger::WARN,
- ]);
-
- return null;
- }
- }
/**
* check if:
@@ -301,11 +188,8 @@ class RefreshWebcalService {
* - the webcal feed suggests a refreshrate
* - return suggested refreshrate if user didn't set a custom one
*
- * @param array $subscription
- * @param string $webcalData
- * @return string|null
*/
- private function checkWebcalDataForRefreshRate($subscription, $webcalData) {
+ private function checkWebcalDataForRefreshRate(array $subscription, string $webcalData): ?string {
// if there is no refreshrate stored in the database, check the webcal feed
// whether it suggests any refresh rate and store that in the database
if (isset($subscription[self::REFRESH_RATE]) && $subscription[self::REFRESH_RATE] !== null) {
@@ -357,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) {
- $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;
}
}