aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/Service
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/Service')
-rw-r--r--apps/dav/lib/Service/AbsenceService.php155
-rw-r--r--apps/dav/lib/Service/ExampleContactService.php132
-rw-r--r--apps/dav/lib/Service/ExampleEventService.php205
3 files changed, 492 insertions, 0 deletions
diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php
new file mode 100644
index 00000000000..7cbc0386d43
--- /dev/null
+++ b/apps/dav/lib/Service/AbsenceService.php
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Service;
+
+use InvalidArgumentException;
+use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob;
+use OCA\DAV\CalDAV\TimezoneService;
+use OCA\DAV\Db\Absence;
+use OCA\DAV\Db\AbsenceMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IUser;
+use OCP\User\Events\OutOfOfficeChangedEvent;
+use OCP\User\Events\OutOfOfficeClearedEvent;
+use OCP\User\Events\OutOfOfficeScheduledEvent;
+use OCP\User\IOutOfOfficeData;
+
+class AbsenceService {
+ public function __construct(
+ private AbsenceMapper $absenceMapper,
+ private IEventDispatcher $eventDispatcher,
+ private IJobList $jobList,
+ private TimezoneService $timezoneService,
+ private ITimeFactory $timeFactory,
+ ) {
+ }
+
+ /**
+ * @param string $firstDay The first day (inclusive) of the absence formatted as YYYY-MM-DD.
+ * @param string $lastDay The last day (inclusive) of the absence formatted as YYYY-MM-DD.
+ *
+ * @throws \OCP\DB\Exception
+ * @throws InvalidArgumentException If no user with the given user id exists.
+ */
+ public function createOrUpdateAbsence(
+ IUser $user,
+ string $firstDay,
+ string $lastDay,
+ string $status,
+ string $message,
+ ?string $replacementUserId = null,
+ ?string $replacementUserDisplayName = null,
+ ): Absence {
+ try {
+ $absence = $this->absenceMapper->findByUserId($user->getUID());
+ } catch (DoesNotExistException) {
+ $absence = new Absence();
+ }
+
+ $absence->setUserId($user->getUID());
+ $absence->setFirstDay($firstDay);
+ $absence->setLastDay($lastDay);
+ $absence->setStatus($status);
+ $absence->setMessage($message);
+ $absence->setReplacementUserId($replacementUserId);
+ $absence->setReplacementUserDisplayName($replacementUserDisplayName);
+
+ if ($absence->getId() === null) {
+ $absence = $this->absenceMapper->insert($absence);
+ $eventData = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ $this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent($eventData));
+ } else {
+ $absence = $this->absenceMapper->update($absence);
+ $eventData = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData));
+ }
+
+ $now = $this->timeFactory->getTime();
+ if ($eventData->getStartDate() > $now) {
+ $this->jobList->scheduleAfter(
+ OutOfOfficeEventDispatcherJob::class,
+ $eventData->getStartDate(),
+ [
+ 'id' => $absence->getId(),
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_START,
+ ],
+ );
+ }
+ if ($eventData->getEndDate() > $now) {
+ $this->jobList->scheduleAfter(
+ OutOfOfficeEventDispatcherJob::class,
+ $eventData->getEndDate(),
+ [
+ 'id' => $absence->getId(),
+ 'event' => OutOfOfficeEventDispatcherJob::EVENT_END,
+ ],
+ );
+ }
+
+ return $absence;
+ }
+
+ /**
+ * @throws \OCP\DB\Exception
+ */
+ public function clearAbsence(IUser $user): void {
+ try {
+ $absence = $this->absenceMapper->findByUserId($user->getUID());
+ } catch (DoesNotExistException $e) {
+ // Nothing to clear
+ return;
+ }
+ $this->absenceMapper->delete($absence);
+ $this->jobList->remove(OutOfOfficeEventDispatcherJob::class);
+ $eventData = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ $this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData));
+ }
+
+ public function getAbsence(string $userId): ?Absence {
+ try {
+ return $this->absenceMapper->findByUserId($userId);
+ } catch (DoesNotExistException $e) {
+ return null;
+ }
+ }
+
+ public function getCurrentAbsence(IUser $user): ?IOutOfOfficeData {
+ try {
+ $absence = $this->absenceMapper->findByUserId($user->getUID());
+ $oooData = $absence->toOutOufOfficeData(
+ $user,
+ $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
+ );
+ if ($this->isInEffect($oooData)) {
+ return $oooData;
+ }
+ } catch (DoesNotExistException) {
+ // Nothing there to process
+ }
+ return null;
+ }
+
+ public function isInEffect(IOutOfOfficeData $absence): bool {
+ $now = $this->timeFactory->getTime();
+ return $absence->getStartDate() <= $now && $absence->getEndDate() >= $now;
+ }
+}
diff --git a/apps/dav/lib/Service/ExampleContactService.php b/apps/dav/lib/Service/ExampleContactService.php
new file mode 100644
index 00000000000..6ed6c66cbb3
--- /dev/null
+++ b/apps/dav/lib/Service/ExampleContactService.php
@@ -0,0 +1,132 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Service;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CardDAV\CardDavBackend;
+use OCP\AppFramework\Services\IAppConfig;
+use OCP\Files\AppData\IAppDataFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Uid\Uuid;
+
+class ExampleContactService {
+ private readonly IAppData $appData;
+
+ public function __construct(
+ IAppDataFactory $appDataFactory,
+ private readonly IAppConfig $appConfig,
+ private readonly LoggerInterface $logger,
+ private readonly CardDavBackend $cardDav,
+ ) {
+ $this->appData = $appDataFactory->get(Application::APP_ID);
+ }
+
+ public function isDefaultContactEnabled(): bool {
+ return $this->appConfig->getAppValueBool('enableDefaultContact', true);
+ }
+
+ public function setDefaultContactEnabled(bool $value): void {
+ $this->appConfig->setAppValueBool('enableDefaultContact', $value);
+ }
+
+ public function getCard(): ?string {
+ try {
+ $folder = $this->appData->getFolder('defaultContact');
+ } catch (NotFoundException $e) {
+ return null;
+ }
+
+ if (!$folder->fileExists('defaultContact.vcf')) {
+ return null;
+ }
+
+ return $folder->getFile('defaultContact.vcf')->getContent();
+ }
+
+ public function setCard(?string $cardData = null) {
+ try {
+ $folder = $this->appData->getFolder('defaultContact');
+ } catch (NotFoundException $e) {
+ $folder = $this->appData->newFolder('defaultContact');
+ }
+
+ $isCustom = true;
+ if (is_null($cardData)) {
+ $cardData = file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf');
+ $isCustom = false;
+ }
+
+ if (!$cardData) {
+ throw new \Exception('Could not read exampleContact.vcf');
+ }
+
+ $file = (!$folder->fileExists('defaultContact.vcf')) ? $folder->newFile('defaultContact.vcf') : $folder->getFile('defaultContact.vcf');
+ $file->putContent($cardData);
+
+ $this->appConfig->setAppValueBool('hasCustomDefaultContact', $isCustom);
+ }
+
+ public function defaultContactExists(): bool {
+ try {
+ $folder = $this->appData->getFolder('defaultContact');
+ } catch (NotFoundException $e) {
+ return false;
+ }
+ return $folder->fileExists('defaultContact.vcf');
+ }
+
+ public function createDefaultContact(int $addressBookId): void {
+ if (!$this->isDefaultContactEnabled()) {
+ return;
+ }
+
+ try {
+ $folder = $this->appData->getFolder('defaultContact');
+ $defaultContactFile = $folder->getFile('defaultContact.vcf');
+ $data = $defaultContactFile->getContent();
+ } catch (\Exception $e) {
+ $this->logger->error('Couldn\'t get default contact file', ['exception' => $e]);
+ return;
+ }
+
+ // Make sure the UID is unique
+ $newUid = Uuid::v4()->toRfc4122();
+ $newRev = date('Ymd\THis\Z');
+ $vcard = \Sabre\VObject\Reader::read($data, \Sabre\VObject\Reader::OPTION_FORGIVING);
+ if ($vcard->UID) {
+ $vcard->UID->setValue($newUid);
+ } else {
+ $vcard->add('UID', $newUid);
+ }
+ if ($vcard->REV) {
+ $vcard->REV->setValue($newRev);
+ } else {
+ $vcard->add('REV', $newRev);
+ }
+
+ // Level 3 means that the document is invalid
+ // https://sabre.io/vobject/vcard/#validating-vcard
+ $level3Warnings = array_filter($vcard->validate(), static function ($warning) {
+ return $warning['level'] === 3;
+ });
+
+ if (!empty($level3Warnings)) {
+ $this->logger->error('Default contact is invalid', ['warnings' => $level3Warnings]);
+ return;
+ }
+ try {
+ $this->cardDav->createCard($addressBookId, 'default', $vcard->serialize(), false);
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ }
+ }
+}
diff --git a/apps/dav/lib/Service/ExampleEventService.php b/apps/dav/lib/Service/ExampleEventService.php
new file mode 100644
index 00000000000..3b2b07fe416
--- /dev/null
+++ b/apps/dav/lib/Service/ExampleEventService.php
@@ -0,0 +1,205 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Service;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Exception\ExampleEventException;
+use OCA\DAV\Model\ExampleEvent;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\IAppConfig;
+use OCP\IL10N;
+use OCP\Security\ISecureRandom;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+
+class ExampleEventService {
+ private const FOLDER_NAME = 'example_event';
+ private const FILE_NAME = 'example_event.ics';
+ private const ENABLE_CONFIG_KEY = 'create_example_event';
+
+ public function __construct(
+ private readonly CalDavBackend $calDavBackend,
+ private readonly ISecureRandom $random,
+ private readonly ITimeFactory $time,
+ private readonly IAppData $appData,
+ private readonly IAppConfig $appConfig,
+ private readonly IL10N $l10n,
+ ) {
+ }
+
+ public function createExampleEvent(int $calendarId): void {
+ if (!$this->shouldCreateExampleEvent()) {
+ return;
+ }
+
+ $exampleEvent = $this->getExampleEvent();
+ $uid = $exampleEvent->getUid();
+ $this->calDavBackend->createCalendarObject(
+ $calendarId,
+ "$uid.ics",
+ $exampleEvent->getIcs(),
+ );
+ }
+
+ private function getStartDate(): \DateTimeInterface {
+ return $this->time->now()
+ ->add(new \DateInterval('P7D'))
+ ->setTime(10, 00);
+ }
+
+ private function getEndDate(): \DateTimeInterface {
+ return $this->time->now()
+ ->add(new \DateInterval('P7D'))
+ ->setTime(11, 00);
+ }
+
+ private function getDefaultEvent(string $uid): VCalendar {
+ $defaultDescription = $this->l10n->t(<<<EOF
+Welcome to Nextcloud Calendar!
+
+This is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!
+
+With Nextcloud Calendar, you can:
+- Create, edit, and manage events effortlessly.
+- Create multiple calendars and share them with teammates, friends, or family.
+- Check availability and display your busy times to others.
+- Seamlessly integrate with apps and devices via CalDAV.
+- Customize your experience: schedule recurring events, adjust notifications and other settings.
+EOF);
+
+ $vCalendar = new VCalendar();
+ $props = [
+ 'UID' => $uid,
+ 'DTSTAMP' => $this->time->now(),
+ 'SUMMARY' => $this->l10n->t('Example event - open me!'),
+ 'DTSTART' => $this->getStartDate(),
+ 'DTEND' => $this->getEndDate(),
+ 'DESCRIPTION' => $defaultDescription,
+ ];
+ $vCalendar->add('VEVENT', $props);
+ return $vCalendar;
+ }
+
+ /**
+ * @return string|null The ics of the custom example event or null if no custom event was uploaded.
+ * @throws ExampleEventException If reading the custom ics file fails.
+ */
+ private function getCustomExampleEvent(): ?string {
+ try {
+ $folder = $this->appData->getFolder(self::FOLDER_NAME);
+ $icsFile = $folder->getFile(self::FILE_NAME);
+ } catch (NotFoundException $e) {
+ return null;
+ }
+
+ try {
+ return $icsFile->getContent();
+ } catch (NotFoundException|NotPermittedException $e) {
+ throw new ExampleEventException(
+ 'Failed to read custom example event',
+ 0,
+ $e,
+ );
+ }
+ }
+
+ /**
+ * Get the configured example event or the default one.
+ *
+ * @throws ExampleEventException If loading the custom example event fails.
+ */
+ public function getExampleEvent(): ExampleEvent {
+ $uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC);
+ $customIcs = $this->getCustomExampleEvent();
+ if ($customIcs === null) {
+ return new ExampleEvent($this->getDefaultEvent($uid), $uid);
+ }
+
+ [$vCalendar, $vEvent] = $this->parseEvent($customIcs);
+ $vEvent->UID = $uid;
+ $vEvent->DTSTART = $this->getStartDate();
+ $vEvent->DTEND = $this->getEndDate();
+ $vEvent->remove('ORGANIZER');
+ $vEvent->remove('ATTENDEE');
+ return new ExampleEvent($vCalendar, $uid);
+ }
+
+ /**
+ * @psalm-return list{VCalendar, VEvent} The VCALENDAR document and its VEVENT child component
+ * @throws ExampleEventException If parsing the event fails or if it is invalid.
+ */
+ private function parseEvent(string $ics): array {
+ try {
+ $vCalendar = \Sabre\VObject\Reader::read($ics);
+ if (!($vCalendar instanceof VCalendar)) {
+ throw new ExampleEventException('Custom event does not contain a VCALENDAR component');
+ }
+
+ /** @var VEvent|null $vEvent */
+ $vEvent = $vCalendar->getBaseComponent('VEVENT');
+ if ($vEvent === null) {
+ throw new ExampleEventException('Custom event does not contain a VEVENT component');
+ }
+ } catch (\Exception $e) {
+ throw new ExampleEventException('Failed to parse custom event: ' . $e->getMessage(), 0, $e);
+ }
+
+ return [$vCalendar, $vEvent];
+ }
+
+ public function saveCustomExampleEvent(string $ics): void {
+ // Parse and validate the event before attempting to save it to prevent run time errors
+ $this->parseEvent($ics);
+
+ try {
+ $folder = $this->appData->getFolder(self::FOLDER_NAME);
+ } catch (NotFoundException $e) {
+ $folder = $this->appData->newFolder(self::FOLDER_NAME);
+ }
+
+ try {
+ $existingFile = $folder->getFile(self::FILE_NAME);
+ $existingFile->putContent($ics);
+ } catch (NotFoundException $e) {
+ $folder->newFile(self::FILE_NAME, $ics);
+ }
+ }
+
+ public function deleteCustomExampleEvent(): void {
+ try {
+ $folder = $this->appData->getFolder(self::FOLDER_NAME);
+ $file = $folder->getFile(self::FILE_NAME);
+ } catch (NotFoundException $e) {
+ return;
+ }
+
+ $file->delete();
+ }
+
+ public function hasCustomExampleEvent(): bool {
+ try {
+ return $this->getCustomExampleEvent() !== null;
+ } catch (ExampleEventException $e) {
+ return false;
+ }
+ }
+
+ public function setCreateExampleEvent(bool $enable): void {
+ $this->appConfig->setValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, $enable);
+ }
+
+ public function shouldCreateExampleEvent(): bool {
+ return $this->appConfig->getValueBool(Application::APP_ID, self::ENABLE_CONFIG_KEY, true);
+ }
+}