aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/Controller
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/Controller')
-rw-r--r--apps/dav/lib/Controller/BirthdayCalendarController.php77
-rw-r--r--apps/dav/lib/Controller/DirectController.php98
-rw-r--r--apps/dav/lib/Controller/ExampleContentController.php98
-rw-r--r--apps/dav/lib/Controller/InvitationResponseController.php93
-rw-r--r--apps/dav/lib/Controller/OutOfOfficeController.php189
-rw-r--r--apps/dav/lib/Controller/UpcomingEventsController.php57
6 files changed, 427 insertions, 185 deletions
diff --git a/apps/dav/lib/Controller/BirthdayCalendarController.php b/apps/dav/lib/Controller/BirthdayCalendarController.php
index 4305d6daaef..f6bfb229a9c 100644
--- a/apps/dav/lib/Controller/BirthdayCalendarController.php
+++ b/apps/dav/lib/Controller/BirthdayCalendarController.php
@@ -1,31 +1,16 @@
<?php
+
/**
- * @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Controller;
use OCA\DAV\BackgroundJob\GenerateBirthdayCalendarBackgroundJob;
use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Settings\CalDAVSettings;
use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\BackgroundJob\IJobList;
@@ -38,31 +23,6 @@ use OCP\IUserManager;
class BirthdayCalendarController extends Controller {
/**
- * @var IDBConnection
- */
- protected $db;
-
- /**
- * @var IConfig
- */
- protected $config;
-
- /**
- * @var IUserManager
- */
- protected $userManager;
-
- /**
- * @var CalDavBackend
- */
- protected $caldavBackend;
-
- /**
- * @var IJobList
- */
- protected $jobList;
-
- /**
* BirthdayCalendar constructor.
*
* @param string $appName
@@ -71,30 +31,29 @@ class BirthdayCalendarController extends Controller {
* @param IConfig $config
* @param IJobList $jobList
* @param IUserManager $userManager
- * @param CalDavBackend $calDavBackend
+ * @param CalDavBackend $caldavBackend
*/
- public function __construct($appName, IRequest $request,
- IDBConnection $db, IConfig $config,
- IJobList $jobList,
- IUserManager $userManager,
- CalDavBackend $calDavBackend) {
+ public function __construct(
+ $appName,
+ IRequest $request,
+ protected IDBConnection $db,
+ protected IConfig $config,
+ protected IJobList $jobList,
+ protected IUserManager $userManager,
+ protected CalDavBackend $caldavBackend,
+ ) {
parent::__construct($appName, $request);
- $this->db = $db;
- $this->config = $config;
- $this->userManager = $userManager;
- $this->jobList = $jobList;
- $this->caldavBackend = $calDavBackend;
}
/**
* @return Response
- * @AuthorizedAdminSetting(settings=OCA\DAV\Settings\CalDAVSettings)
*/
+ #[AuthorizedAdminSetting(settings: CalDAVSettings::class)]
public function enable() {
$this->config->setAppValue($this->appName, 'generateBirthdayCalendar', 'yes');
// add background job for each user
- $this->userManager->callForSeenUsers(function (IUser $user) {
+ $this->userManager->callForSeenUsers(function (IUser $user): void {
$this->jobList->add(GenerateBirthdayCalendarBackgroundJob::class, [
'userId' => $user->getUID(),
]);
@@ -105,8 +64,8 @@ class BirthdayCalendarController extends Controller {
/**
* @return Response
- * @AuthorizedAdminSetting(settings=OCA\DAV\Settings\CalDAVSettings)
*/
+ #[AuthorizedAdminSetting(settings: CalDAVSettings::class)]
public function disable() {
$this->config->setAppValue($this->appName, 'generateBirthdayCalendar', 'no');
diff --git a/apps/dav/lib/Controller/DirectController.php b/apps/dav/lib/Controller/DirectController.php
index 955400998cf..ea209168123 100644
--- a/apps/dav/lib/Controller/DirectController.php
+++ b/apps/dav/lib/Controller/DirectController.php
@@ -3,36 +3,23 @@
declare(strict_types=1);
/**
- * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author Iscle <albertiscle9@gmail.com>
- * @author Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Controller;
use OCA\DAV\Db\Direct;
use OCA\DAV\Db\DirectMapper;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
+use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IRequest;
@@ -41,52 +28,39 @@ use OCP\Security\ISecureRandom;
class DirectController extends OCSController {
- /** @var IRootFolder */
- private $rootFolder;
-
- /** @var string */
- private $userId;
-
- /** @var DirectMapper */
- private $mapper;
-
- /** @var ISecureRandom */
- private $random;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var IURLGenerator */
- private $urlGenerator;
-
-
- public function __construct(string $appName,
- IRequest $request,
- IRootFolder $rootFolder,
- string $userId,
- DirectMapper $mapper,
- ISecureRandom $random,
- ITimeFactory $timeFactory,
- IURLGenerator $urlGenerator) {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IRootFolder $rootFolder,
+ private string $userId,
+ private DirectMapper $mapper,
+ private ISecureRandom $random,
+ private ITimeFactory $timeFactory,
+ private IURLGenerator $urlGenerator,
+ private IEventDispatcher $eventDispatcher,
+ ) {
parent::__construct($appName, $request);
-
- $this->rootFolder = $rootFolder;
- $this->userId = $userId;
- $this->mapper = $mapper;
- $this->random = $random;
- $this->timeFactory = $timeFactory;
- $this->urlGenerator = $urlGenerator;
}
/**
- * @NoAdminRequired
+ * Get a direct link to a file
+ *
+ * @param int $fileId ID of the file
+ * @param int $expirationTime Duration until the link expires
+ * @return DataResponse<Http::STATUS_OK, array{url: string}, array{}>
+ * @throws OCSNotFoundException File not found
+ * @throws OCSBadRequestException Getting direct link is not possible
+ * @throws OCSForbiddenException Missing permissions to get direct link
+ *
+ * 200: Direct link returned
*/
+ #[NoAdminRequired]
public function getUrl(int $fileId, int $expirationTime = 60 * 60 * 8): DataResponse {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
- $files = $userFolder->getById($fileId);
+ $file = $userFolder->getFirstNodeById($fileId);
- if ($files === []) {
+ if (!$file) {
throw new OCSNotFoundException();
}
@@ -94,11 +68,17 @@ class DirectController extends OCSController {
throw new OCSBadRequestException('Expiration time should be greater than 0 and less than or equal to ' . (60 * 60 * 24));
}
- $file = array_shift($files);
if (!($file instanceof File)) {
throw new OCSBadRequestException('Direct download only works for files');
}
+ $event = new BeforeDirectFileDownloadEvent($userFolder->getRelativePath($file->getPath()));
+ $this->eventDispatcher->dispatchTyped($event);
+
+ if ($event->isSuccessful() === false) {
+ throw new OCSForbiddenException('Permission denied to download file');
+ }
+
//TODO: at some point we should use the directdownlaod function of storages
$direct = new Direct();
$direct->setUserId($this->userId);
@@ -110,7 +90,7 @@ class DirectController extends OCSController {
$this->mapper->insert($direct);
- $url = $this->urlGenerator->getAbsoluteURL('remote.php/direct/'.$token);
+ $url = $this->urlGenerator->getAbsoluteURL('remote.php/direct/' . $token);
return new DataResponse([
'url' => $url,
diff --git a/apps/dav/lib/Controller/ExampleContentController.php b/apps/dav/lib/Controller/ExampleContentController.php
new file mode 100644
index 00000000000..e20ee4b7f49
--- /dev/null
+++ b/apps/dav/lib/Controller/ExampleContentController.php
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Controller;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\Service\ExampleContactService;
+use OCA\DAV\Service\ExampleEventService;
+use OCP\AppFramework\ApiController;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\FrontpageRoute;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\DataDownloadResponse;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use Psr\Log\LoggerInterface;
+
+class ExampleContentController extends ApiController {
+ public function __construct(
+ IRequest $request,
+ private readonly LoggerInterface $logger,
+ private readonly ExampleEventService $exampleEventService,
+ private readonly ExampleContactService $exampleContactService,
+ ) {
+ parent::__construct(Application::APP_ID, $request);
+ }
+
+ #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/config')]
+ public function setEnableDefaultContact(bool $allow): JSONResponse {
+ if ($allow && !$this->exampleContactService->defaultContactExists()) {
+ try {
+ $this->exampleContactService->setCard();
+ } catch (\Exception $e) {
+ $this->logger->error('Could not create default contact', ['exception' => $e]);
+ return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+ }
+ $this->exampleContactService->setDefaultContactEnabled($allow);
+ return new JSONResponse([], Http::STATUS_OK);
+ }
+
+ #[NoCSRFRequired]
+ #[FrontpageRoute(verb: 'GET', url: '/api/defaultcontact/contact')]
+ public function getDefaultContact(): DataDownloadResponse {
+ $cardData = $this->exampleContactService->getCard()
+ ?? file_get_contents(__DIR__ . '/../ExampleContentFiles/exampleContact.vcf');
+ return new DataDownloadResponse($cardData, 'example_contact.vcf', 'text/vcard');
+ }
+
+ #[FrontpageRoute(verb: 'PUT', url: '/api/defaultcontact/contact')]
+ public function setDefaultContact(?string $contactData = null) {
+ if (!$this->exampleContactService->isDefaultContactEnabled()) {
+ return new JSONResponse([], Http::STATUS_FORBIDDEN);
+ }
+ $this->exampleContactService->setCard($contactData);
+ return new JSONResponse([], Http::STATUS_OK);
+ }
+
+ #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/enable')]
+ public function setCreateExampleEvent(bool $enable): JSONResponse {
+ $this->exampleEventService->setCreateExampleEvent($enable);
+ return new JsonResponse([]);
+ }
+
+ #[FrontpageRoute(verb: 'GET', url: '/api/exampleEvent/event')]
+ #[NoCSRFRequired]
+ public function downloadExampleEvent(): DataDownloadResponse {
+ $exampleEvent = $this->exampleEventService->getExampleEvent();
+ return new DataDownloadResponse(
+ $exampleEvent->getIcs(),
+ 'example_event.ics',
+ 'text/calendar',
+ );
+ }
+
+ #[FrontpageRoute(verb: 'POST', url: '/api/exampleEvent/event')]
+ public function uploadExampleEvent(string $ics): JSONResponse {
+ if (!$this->exampleEventService->shouldCreateExampleEvent()) {
+ return new JSONResponse([], Http::STATUS_FORBIDDEN);
+ }
+
+ $this->exampleEventService->saveCustomExampleEvent($ics);
+ return new JsonResponse([]);
+ }
+
+ #[FrontpageRoute(verb: 'DELETE', url: '/api/exampleEvent/event')]
+ public function deleteExampleEvent(): JSONResponse {
+ $this->exampleEventService->deleteCustomExampleEvent();
+ return new JsonResponse([]);
+ }
+
+}
diff --git a/apps/dav/lib/Controller/InvitationResponseController.php b/apps/dav/lib/Controller/InvitationResponseController.php
index de22e3ba6a9..19eb4097b45 100644
--- a/apps/dav/lib/Controller/InvitationResponseController.php
+++ b/apps/dav/lib/Controller/InvitationResponseController.php
@@ -3,32 +3,16 @@
declare(strict_types=1);
/**
- * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
- *
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Joas Schilling <coding@schilljs.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Controller;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
+use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IDBConnection;
@@ -36,17 +20,9 @@ use OCP\IRequest;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Reader;
+#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class InvitationResponseController extends Controller {
- /** @var IDBConnection */
- private $db;
-
- /** @var ITimeFactory */
- private $timeFactory;
-
- /** @var InvitationResponseServer */
- private $responseServer;
-
/**
* InvitationResponseController constructor.
*
@@ -56,25 +32,25 @@ class InvitationResponseController extends Controller {
* @param ITimeFactory $timeFactory
* @param InvitationResponseServer $responseServer
*/
- public function __construct(string $appName, IRequest $request,
- IDBConnection $db, ITimeFactory $timeFactory,
- InvitationResponseServer $responseServer) {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IDBConnection $db,
+ private ITimeFactory $timeFactory,
+ private InvitationResponseServer $responseServer,
+ ) {
parent::__construct($appName, $request);
- $this->db = $db;
- $this->timeFactory = $timeFactory;
- $this->responseServer = $responseServer;
// Don't run `$server->exec()`, because we just need access to the
// fully initialized schedule plugin, but we don't want Sabre/DAV
// to actually handle and reply to the request
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function accept(string $token):TemplateResponse {
$row = $this->getTokenInformation($token);
if (!$row) {
@@ -93,12 +69,11 @@ class InvitationResponseController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function decline(string $token):TemplateResponse {
$row = $this->getTokenInformation($token);
if (!$row) {
@@ -118,12 +93,11 @@ class InvitationResponseController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function options(string $token):TemplateResponse {
return new TemplateResponse($this->appName, 'schedule-response-options', [
'token' => $token
@@ -131,24 +105,21 @@ class InvitationResponseController extends Controller {
}
/**
- * @PublicPage
- * @NoCSRFRequired
- *
* @param string $token
*
* @return TemplateResponse
*/
+ #[PublicPage]
+ #[NoCSRFRequired]
public function processMoreOptionsResult(string $token):TemplateResponse {
$partstat = $this->request->getParam('partStat');
- $guests = (int) $this->request->getParam('guests');
- $comment = $this->request->getParam('comment');
$row = $this->getTokenInformation($token);
if (!$row || !\in_array($partstat, ['ACCEPTED', 'DECLINED', 'TENTATIVE'])) {
return new TemplateResponse($this->appName, 'schedule-response-error', [], 'guest');
}
- $iTipMessage = $this->buildITipResponse($row, $partstat, $guests, $comment);
+ $iTipMessage = $this->buildITipResponse($row, $partstat);
$this->responseServer->handleITipMessage($iTipMessage);
if ($iTipMessage->getScheduleStatus() === '1.2') {
return new TemplateResponse($this->appName, 'schedule-response-success', [], 'guest');
@@ -168,15 +139,16 @@ class InvitationResponseController extends Controller {
$query->select('*')
->from('calendar_invitations')
->where($query->expr()->eq('token', $query->createNamedParameter($token)));
- $stmt = $query->execute();
+ $stmt = $query->executeQuery();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
+ $stmt->closeCursor();
if (!$row) {
return null;
}
$currentTime = $this->timeFactory->getTime();
- if (((int) $row['expiration']) < $currentTime) {
+ if (((int)$row['expiration']) < $currentTime) {
return null;
}
@@ -190,8 +162,7 @@ class InvitationResponseController extends Controller {
* @param string|null $comment
* @return Message
*/
- private function buildITipResponse(array $row, string $partStat, int $guests = null,
- string $comment = null):Message {
+ private function buildITipResponse(array $row, string $partStat):Message {
$iTipMessage = new Message();
$iTipMessage->uid = $row['uid'];
$iTipMessage->component = 'VEVENT';
@@ -225,19 +196,7 @@ EOF;
$row['uid'], $row['sequence'] ?? 0, $row['recurrenceid'] ?? ''
]));
$vEvent = $vObject->{'VEVENT'};
- /** @var \Sabre\VObject\Property\ICalendar\CalAddress $attendee */
- $attendee = $vEvent->{'ATTENDEE'};
-
$vEvent->DTSTAMP = date('Ymd\\THis\\Z', $this->timeFactory->getTime());
-
- if ($comment) {
- $attendee->add('X-RESPONSE-COMMENT', $comment);
- $vEvent->add('COMMENT', $comment);
- }
- if ($guests) {
- $attendee->add('X-NUM-GUESTS', $guests);
- }
-
$iTipMessage->message = $vObject;
return $iTipMessage;
diff --git a/apps/dav/lib/Controller/OutOfOfficeController.php b/apps/dav/lib/Controller/OutOfOfficeController.php
new file mode 100644
index 00000000000..d3516d092e8
--- /dev/null
+++ b/apps/dav/lib/Controller/OutOfOfficeController.php
@@ -0,0 +1,189 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Controller;
+
+use DateTimeImmutable;
+use OCA\DAV\ResponseDefinitions;
+use OCA\DAV\Service\AbsenceService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\IRequest;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\User\IAvailabilityCoordinator;
+use function mb_strlen;
+
+/**
+ * @psalm-import-type DAVOutOfOfficeData from ResponseDefinitions
+ * @psalm-import-type DAVCurrentOutOfOfficeData from ResponseDefinitions
+ */
+class OutOfOfficeController extends OCSController {
+
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private IUserManager $userManager,
+ private ?IUserSession $userSession,
+ private AbsenceService $absenceService,
+ private IAvailabilityCoordinator $coordinator,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * Get the currently configured out-of-office data of a user
+ *
+ * @param string $userId The user id to get out-of-office data for.
+ * @return DataResponse<Http::STATUS_OK, DAVCurrentOutOfOfficeData, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
+ *
+ * 200: Out-of-office data
+ * 404: No out-of-office data was found
+ */
+ #[NoAdminRequired]
+ public function getCurrentOutOfOfficeData(string $userId): DataResponse {
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+ try {
+ $data = $this->absenceService->getCurrentAbsence($user);
+ if ($data === null) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+ } catch (DoesNotExistException) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+
+ return new DataResponse($data->jsonSerialize());
+ }
+
+ /**
+ * Get the configured out-of-office data of a user.
+ *
+ * @param string $userId The user id to get out-of-office data for.
+ * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
+ *
+ * 200: Out-of-office data
+ * 404: No out-of-office data was found
+ */
+ #[NoAdminRequired]
+ public function getOutOfOffice(string $userId): DataResponse {
+ try {
+ $data = $this->absenceService->getAbsence($userId);
+ if ($data === null) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+ } catch (DoesNotExistException) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+
+ return new DataResponse([
+ 'id' => $data->getId(),
+ 'userId' => $data->getUserId(),
+ 'firstDay' => $data->getFirstDay(),
+ 'lastDay' => $data->getLastDay(),
+ 'status' => $data->getStatus(),
+ 'message' => $data->getMessage(),
+ 'replacementUserId' => $data->getReplacementUserId(),
+ 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(),
+ ]);
+ }
+
+ /**
+ * Set out-of-office absence
+ *
+ * @param string $firstDay First day of the absence in format `YYYY-MM-DD`
+ * @param string $lastDay Last day of the absence in format `YYYY-MM-DD`
+ * @param string $status Short text that is set as user status during the absence
+ * @param string $message Longer multiline message that is shown to others during the absence
+ * @param ?string $replacementUserId User id of the replacement user
+ * @return DataResponse<Http::STATUS_OK, DAVOutOfOfficeData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'firstDay'|'statusLength'}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
+ *
+ * 200: Absence data
+ * 400: When validation fails, e.g. data range error or the first day is not before the last day
+ * 401: When the user is not logged in
+ * 404: When the replacementUserId was provided but replacement user was not found
+ */
+ #[NoAdminRequired]
+ public function setOutOfOffice(
+ string $firstDay,
+ string $lastDay,
+ string $status,
+ string $message,
+ ?string $replacementUserId,
+ ): DataResponse {
+ $user = $this->userSession?->getUser();
+ if ($user === null) {
+ return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
+ }
+ if (mb_strlen($status) > 100) {
+ return new DataResponse(['error' => 'statusLength'], Http::STATUS_BAD_REQUEST);
+ }
+
+ $replacementUser = null;
+ if ($replacementUserId !== null) {
+ $replacementUser = $this->userManager->get($replacementUserId);
+ if ($replacementUser === null) {
+ return new DataResponse(null, Http::STATUS_NOT_FOUND);
+ }
+ }
+
+ $parsedFirstDay = new DateTimeImmutable($firstDay);
+ $parsedLastDay = new DateTimeImmutable($lastDay);
+ if ($parsedFirstDay->getTimestamp() > $parsedLastDay->getTimestamp()) {
+ return new DataResponse(['error' => 'firstDay'], Http::STATUS_BAD_REQUEST);
+ }
+
+ $data = $this->absenceService->createOrUpdateAbsence(
+ $user,
+ $firstDay,
+ $lastDay,
+ $status,
+ $message,
+ $replacementUserId,
+ $replacementUser?->getDisplayName()
+ );
+ $this->coordinator->clearCache($user->getUID());
+
+ return new DataResponse([
+ 'id' => $data->getId(),
+ 'userId' => $data->getUserId(),
+ 'firstDay' => $data->getFirstDay(),
+ 'lastDay' => $data->getLastDay(),
+ 'status' => $data->getStatus(),
+ 'message' => $data->getMessage(),
+ 'replacementUserId' => $data->getReplacementUserId(),
+ 'replacementUserDisplayName' => $data->getReplacementUserDisplayName(),
+ ]);
+ }
+
+ /**
+ * Clear the out-of-office
+ *
+ * @return DataResponse<Http::STATUS_OK|Http::STATUS_UNAUTHORIZED, null, array{}>
+ *
+ * 200: When the absence was cleared successfully
+ * 401: When the user is not logged in
+ */
+ #[NoAdminRequired]
+ public function clearOutOfOffice(): DataResponse {
+ $user = $this->userSession?->getUser();
+ if ($user === null) {
+ return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
+ }
+
+ $this->absenceService->clearAbsence($user);
+ $this->coordinator->clearCache($user->getUID());
+ return new DataResponse(null);
+ }
+}
diff --git a/apps/dav/lib/Controller/UpcomingEventsController.php b/apps/dav/lib/Controller/UpcomingEventsController.php
new file mode 100644
index 00000000000..a5d54f44754
--- /dev/null
+++ b/apps/dav/lib/Controller/UpcomingEventsController.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\Controller;
+
+use OCA\DAV\AppInfo\Application;
+use OCA\DAV\CalDAV\UpcomingEvent;
+use OCA\DAV\CalDAV\UpcomingEventsService;
+use OCA\DAV\ResponseDefinitions;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\IRequest;
+
+/**
+ * @psalm-import-type DAVUpcomingEvent from ResponseDefinitions
+ */
+class UpcomingEventsController extends OCSController {
+ public function __construct(
+ IRequest $request,
+ private ?string $userId,
+ private UpcomingEventsService $service,
+ ) {
+ parent::__construct(Application::APP_ID, $request);
+ }
+
+ /**
+ * Get information about upcoming events
+ *
+ * @param string|null $location location/URL to filter by
+ * @return DataResponse<Http::STATUS_OK, array{events: list<DAVUpcomingEvent>}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, null, array{}>
+ *
+ * 200: Upcoming events
+ * 401: When not authenticated
+ */
+ #[NoAdminRequired]
+ public function getEvents(?string $location = null): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse(null, Http::STATUS_UNAUTHORIZED);
+ }
+
+ return new DataResponse([
+ 'events' => array_values(array_map(fn (UpcomingEvent $e) => $e->jsonSerialize(), $this->service->getEvents(
+ $this->userId,
+ $location,
+ ))),
+ ]);
+ }
+
+}