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