diff options
-rw-r--r-- | apps/dav/appinfo/routes.php | 1 | ||||
-rw-r--r-- | apps/dav/composer/composer/autoload_classmap.php | 2 | ||||
-rw-r--r-- | apps/dav/composer/composer/autoload_static.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/Controller/OutOfOfficeController.php | 78 | ||||
-rw-r--r-- | apps/dav/lib/Db/Absence.php | 28 | ||||
-rw-r--r-- | apps/dav/lib/ResponseDefinitions.php | 40 | ||||
-rw-r--r-- | apps/dav/lib/Service/AbsenceService.php | 37 | ||||
-rw-r--r-- | apps/dav/openapi.json | 134 | ||||
-rw-r--r-- | lib/composer/composer/autoload_classmap.php | 7 | ||||
-rw-r--r-- | lib/composer/composer/autoload_static.php | 7 | ||||
-rw-r--r-- | lib/private/User/AvailabilityCoordinator.php | 111 | ||||
-rw-r--r-- | lib/private/User/OutOfOfficeData.php | 63 | ||||
-rw-r--r-- | lib/public/User/Events/OutOfOfficeChangedEvent.php | 50 | ||||
-rw-r--r-- | lib/public/User/Events/OutOfOfficeClearedEvent.php | 50 | ||||
-rw-r--r-- | lib/public/User/Events/OutOfOfficeScheduledEvent.php | 50 | ||||
-rw-r--r-- | lib/public/User/IAvailabilityCoordinator.php | 42 | ||||
-rw-r--r-- | lib/public/User/IOutOfOfficeData.php | 77 | ||||
-rw-r--r-- | tests/lib/User/AvailabilityCoordinatorTest.php | 164 |
18 files changed, 942 insertions, 1 deletions
diff --git a/apps/dav/appinfo/routes.php b/apps/dav/appinfo/routes.php index 3236c0642af..1b2fa0094bf 100644 --- a/apps/dav/appinfo/routes.php +++ b/apps/dav/appinfo/routes.php @@ -35,5 +35,6 @@ return [ ], 'ocs' => [ ['name' => 'direct#getUrl', 'url' => '/api/v1/direct', 'verb' => 'POST'], + ['name' => 'out_of_office#getCurrentOutOfOfficeData', 'url' => '/api/v1/outOfOffice/{userId}', 'verb' => 'GET'], ], ]; diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index e0c3e20dc6b..bb424e47787 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -192,6 +192,7 @@ return array( 'OCA\\DAV\\Controller\\BirthdayCalendarController' => $baseDir . '/../lib/Controller/BirthdayCalendarController.php', 'OCA\\DAV\\Controller\\DirectController' => $baseDir . '/../lib/Controller/DirectController.php', 'OCA\\DAV\\Controller\\InvitationResponseController' => $baseDir . '/../lib/Controller/InvitationResponseController.php', + 'OCA\\DAV\\Controller\\OutOfOfficeController' => $baseDir . '/../lib/Controller/OutOfOfficeController.php', 'OCA\\DAV\\DAV\\CustomPropertiesBackend' => $baseDir . '/../lib/DAV/CustomPropertiesBackend.php', 'OCA\\DAV\\DAV\\GroupPrincipalBackend' => $baseDir . '/../lib/DAV/GroupPrincipalBackend.php', 'OCA\\DAV\\DAV\\PublicAuth' => $baseDir . '/../lib/DAV/PublicAuth.php', @@ -305,6 +306,7 @@ return array( 'OCA\\DAV\\Profiler\\ProfilerPlugin' => $baseDir . '/../lib/Profiler/ProfilerPlugin.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', + 'OCA\\DAV\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', 'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php', 'OCA\\DAV\\Search\\ACalendarSearchProvider' => $baseDir . '/../lib/Search/ACalendarSearchProvider.php', 'OCA\\DAV\\Search\\ContactsSearchProvider' => $baseDir . '/../lib/Search/ContactsSearchProvider.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 9292731af98..ef040e79674 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -207,6 +207,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Controller\\BirthdayCalendarController' => __DIR__ . '/..' . '/../lib/Controller/BirthdayCalendarController.php', 'OCA\\DAV\\Controller\\DirectController' => __DIR__ . '/..' . '/../lib/Controller/DirectController.php', 'OCA\\DAV\\Controller\\InvitationResponseController' => __DIR__ . '/..' . '/../lib/Controller/InvitationResponseController.php', + 'OCA\\DAV\\Controller\\OutOfOfficeController' => __DIR__ . '/..' . '/../lib/Controller/OutOfOfficeController.php', 'OCA\\DAV\\DAV\\CustomPropertiesBackend' => __DIR__ . '/..' . '/../lib/DAV/CustomPropertiesBackend.php', 'OCA\\DAV\\DAV\\GroupPrincipalBackend' => __DIR__ . '/..' . '/../lib/DAV/GroupPrincipalBackend.php', 'OCA\\DAV\\DAV\\PublicAuth' => __DIR__ . '/..' . '/../lib/DAV/PublicAuth.php', @@ -320,6 +321,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Profiler\\ProfilerPlugin' => __DIR__ . '/..' . '/../lib/Profiler/ProfilerPlugin.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', + 'OCA\\DAV\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', 'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php', 'OCA\\DAV\\Search\\ACalendarSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ACalendarSearchProvider.php', 'OCA\\DAV\\Search\\ContactsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/ContactsSearchProvider.php', diff --git a/apps/dav/lib/Controller/OutOfOfficeController.php b/apps/dav/lib/Controller/OutOfOfficeController.php new file mode 100644 index 00000000000..fe4200ee1b5 --- /dev/null +++ b/apps/dav/lib/Controller/OutOfOfficeController.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> + * + * @author Richard Steinmetz <richard@steinmetz.cloud> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\Controller; + +use OCA\DAV\Db\AbsenceMapper; +use OCA\DAV\ResponseDefinitions; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +/** + * @psalm-import-type DAVOutOfOfficeData from ResponseDefinitions + */ +class OutOfOfficeController extends OCSController { + + public function __construct( + string $appName, + IRequest $request, + private AbsenceMapper $absenceMapper, + ) { + parent::__construct($appName, $request); + } + + /** + * Get the currently configured out-of-office data of a user. + * + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $userId The user id to get out-of-office data for. + * @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, ?DAVOutOfOfficeData, array{}> + * + * 200: Out-of-office data + * 404: No out-of-office data was found + */ + public function getCurrentOutOfOfficeData(string $userId): DataResponse { + try { + $data = $this->absenceMapper->findByUserId($userId); + } 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(), + ]); + } +} diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php index f705b99ef30..e9ce1d2ea64 100644 --- a/apps/dav/lib/Db/Absence.php +++ b/apps/dav/lib/Db/Absence.php @@ -26,8 +26,13 @@ declare(strict_types=1); namespace OCA\DAV\Db; +use DateTimeImmutable; +use InvalidArgumentException; use JsonSerializable; +use OC\User\OutOfOfficeData; use OCP\AppFramework\Db\Entity; +use OCP\IUser; +use OCP\User\IOutOfOfficeData; /** * @method string getUserId() @@ -43,8 +48,13 @@ use OCP\AppFramework\Db\Entity; */ class Absence extends Entity implements JsonSerializable { protected string $userId = ''; + + /** Inclusive, formatted as YYYY-MM-DD */ protected string $firstDay = ''; + + /** Inclusive, formatted as YYYY-MM-DD */ protected string $lastDay = ''; + protected string $status = ''; protected string $message = ''; @@ -56,6 +66,24 @@ class Absence extends Entity implements JsonSerializable { $this->addType('message', 'string'); } + public function toOutOufOfficeData(IUser $user): IOutOfOfficeData { + if ($user->getUID() !== $this->getUserId()) { + throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ", got " . $user->getUID()); + } + + //$user = $userManager->get($this->getUserId()); + $startDate = new DateTimeImmutable($this->getFirstDay()); + $endDate = new DateTimeImmutable($this->getLastDay()); + return new OutOfOfficeData( + (string)$this->getId(), + $user, + $startDate->getTimestamp(), + $endDate->getTimestamp(), + $this->getStatus(), + $this->getMessage(), + ); + } + public function jsonSerialize(): array { return [ 'userId' => $this->userId, diff --git a/apps/dav/lib/ResponseDefinitions.php b/apps/dav/lib/ResponseDefinitions.php new file mode 100644 index 00000000000..97bd8e9efe9 --- /dev/null +++ b/apps/dav/lib/ResponseDefinitions.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> + * + * @author Richard Steinmetz <richard@steinmetz.cloud> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV; + +/** + * @psalm-type DAVOutOfOfficeData = array{ + * id: int, + * userId: string, + * firstDay: string, + * lastDay: string, + * status: string, + * message: string, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php index 228007b3af1..69dee1bd8cc 100644 --- a/apps/dav/lib/Service/AbsenceService.php +++ b/apps/dav/lib/Service/AbsenceService.php @@ -26,18 +26,30 @@ declare(strict_types=1); namespace OCA\DAV\Service; +use InvalidArgumentException; use OCA\DAV\Db\Absence; use OCA\DAV\Db\AbsenceMapper; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUserManager; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; class AbsenceService { public function __construct( private AbsenceMapper $absenceMapper, + private IEventDispatcher $eventDispatcher, + private IUserManager $userManager, ) { } /** + * @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( string $userId, @@ -58,9 +70,19 @@ class AbsenceService { $absence->setStatus($status); $absence->setMessage($message); + // TODO: this method should probably just take a IUser instance + $user = $this->userManager->get($userId); + if ($user === null) { + throw new InvalidArgumentException("User $userId does not exist"); + } + $eventData = $absence->toOutOufOfficeData($user); + if ($absence->getId() === null) { + $this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent($eventData)); return $this->absenceMapper->insert($absence); } + + $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData)); return $this->absenceMapper->update($absence); } @@ -68,7 +90,20 @@ class AbsenceService { * @throws \OCP\DB\Exception */ public function clearAbsence(string $userId): void { - $this->absenceMapper->deleteByUserId($userId); + try { + $absence = $this->absenceMapper->findByUserId($userId); + } catch (DoesNotExistException $e) { + // Nothing to clear + return; + } + $this->absenceMapper->delete($absence); + // TODO: this method should probably just take a IUser instance + $user = $this->userManager->get($userId); + if ($user === null) { + throw new InvalidArgumentException("User $userId does not exist"); + } + $eventData = $absence->toOutOufOfficeData($user); + $this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData)); } } diff --git a/apps/dav/openapi.json b/apps/dav/openapi.json index 09da0c19ff3..994d0cde11d 100644 --- a/apps/dav/openapi.json +++ b/apps/dav/openapi.json @@ -65,6 +65,38 @@ "type": "string" } } + }, + "OutOfOfficeData": { + "type": "object", + "required": [ + "id", + "userId", + "firstDay", + "lastDay", + "status", + "message" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "userId": { + "type": "string" + }, + "firstDay": { + "type": "string" + }, + "lastDay": { + "type": "string" + }, + "status": { + "type": "string" + }, + "message": { + "type": "string" + } + } } } }, @@ -186,6 +218,108 @@ } } } + }, + "/ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId}": { + "get": { + "operationId": "out_of_office-get-current-out-of-office-data", + "summary": "Get the currently configured out-of-office data of a user.", + "tags": [ + "out_of_office" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The user id to get out-of-office data for.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Out-of-office data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/OutOfOfficeData", + "nullable": true + } + } + } + } + } + } + } + }, + "404": { + "description": "No out-of-office data was found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "$ref": "#/components/schemas/OutOfOfficeData", + "nullable": true + } + } + } + } + } + } + } + } + } + } } }, "tags": [] diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 1a55209c286..9757095dc04 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -731,6 +731,9 @@ return array( 'OCP\\User\\Events\\BeforeUserLoggedInEvent' => $baseDir . '/lib/public/User/Events/BeforeUserLoggedInEvent.php', 'OCP\\User\\Events\\BeforeUserLoggedInWithCookieEvent' => $baseDir . '/lib/public/User/Events/BeforeUserLoggedInWithCookieEvent.php', 'OCP\\User\\Events\\BeforeUserLoggedOutEvent' => $baseDir . '/lib/public/User/Events/BeforeUserLoggedOutEvent.php', + 'OCP\\User\\Events\\OutOfOfficeChangedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeChangedEvent.php', + 'OCP\\User\\Events\\OutOfOfficeClearedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeClearedEvent.php', + 'OCP\\User\\Events\\OutOfOfficeScheduledEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeScheduledEvent.php', 'OCP\\User\\Events\\PasswordUpdatedEvent' => $baseDir . '/lib/public/User/Events/PasswordUpdatedEvent.php', 'OCP\\User\\Events\\PostLoginEvent' => $baseDir . '/lib/public/User/Events/PostLoginEvent.php', 'OCP\\User\\Events\\UserChangedEvent' => $baseDir . '/lib/public/User/Events/UserChangedEvent.php', @@ -742,6 +745,8 @@ return array( 'OCP\\User\\Events\\UserLoggedInWithCookieEvent' => $baseDir . '/lib/public/User/Events/UserLoggedInWithCookieEvent.php', 'OCP\\User\\Events\\UserLoggedOutEvent' => $baseDir . '/lib/public/User/Events/UserLoggedOutEvent.php', 'OCP\\User\\GetQuotaEvent' => $baseDir . '/lib/public/User/GetQuotaEvent.php', + 'OCP\\User\\IAvailabilityCoordinator' => $baseDir . '/lib/public/User/IAvailabilityCoordinator.php', + 'OCP\\User\\IOutOfOfficeData' => $baseDir . '/lib/public/User/IOutOfOfficeData.php', 'OCP\\Util' => $baseDir . '/lib/public/Util.php', 'OCP\\WorkflowEngine\\EntityContext\\IContextPortation' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IContextPortation.php', 'OCP\\WorkflowEngine\\EntityContext\\IDisplayName' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IDisplayName.php', @@ -1762,6 +1767,7 @@ return array( 'OC\\Updater\\VersionCheck' => $baseDir . '/lib/private/Updater/VersionCheck.php', 'OC\\UserStatus\\ISettableProvider' => $baseDir . '/lib/private/UserStatus/ISettableProvider.php', 'OC\\UserStatus\\Manager' => $baseDir . '/lib/private/UserStatus/Manager.php', + 'OC\\User\\AvailabilityCoordinator' => $baseDir . '/lib/private/User/AvailabilityCoordinator.php', 'OC\\User\\Backend' => $baseDir . '/lib/private/User/Backend.php', 'OC\\User\\Database' => $baseDir . '/lib/private/User/Database.php', 'OC\\User\\DisplayNameCache' => $baseDir . '/lib/private/User/DisplayNameCache.php', @@ -1771,6 +1777,7 @@ return array( 'OC\\User\\LoginException' => $baseDir . '/lib/private/User/LoginException.php', 'OC\\User\\Manager' => $baseDir . '/lib/private/User/Manager.php', 'OC\\User\\NoUserException' => $baseDir . '/lib/private/User/NoUserException.php', + 'OC\\User\\OutOfOfficeData' => $baseDir . '/lib/private/User/OutOfOfficeData.php', 'OC\\User\\Session' => $baseDir . '/lib/private/User/Session.php', 'OC\\User\\User' => $baseDir . '/lib/private/User/User.php', 'OC_API' => $baseDir . '/lib/private/legacy/OC_API.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index bebf5be91bd..a0492513f8c 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -764,6 +764,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\User\\Events\\BeforeUserLoggedInEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforeUserLoggedInEvent.php', 'OCP\\User\\Events\\BeforeUserLoggedInWithCookieEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforeUserLoggedInWithCookieEvent.php', 'OCP\\User\\Events\\BeforeUserLoggedOutEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforeUserLoggedOutEvent.php', + 'OCP\\User\\Events\\OutOfOfficeChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeChangedEvent.php', + 'OCP\\User\\Events\\OutOfOfficeClearedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeClearedEvent.php', + 'OCP\\User\\Events\\OutOfOfficeScheduledEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeScheduledEvent.php', 'OCP\\User\\Events\\PasswordUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PasswordUpdatedEvent.php', 'OCP\\User\\Events\\PostLoginEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PostLoginEvent.php', 'OCP\\User\\Events\\UserChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserChangedEvent.php', @@ -775,6 +778,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\User\\Events\\UserLoggedInWithCookieEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLoggedInWithCookieEvent.php', 'OCP\\User\\Events\\UserLoggedOutEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLoggedOutEvent.php', 'OCP\\User\\GetQuotaEvent' => __DIR__ . '/../../..' . '/lib/public/User/GetQuotaEvent.php', + 'OCP\\User\\IAvailabilityCoordinator' => __DIR__ . '/../../..' . '/lib/public/User/IAvailabilityCoordinator.php', + 'OCP\\User\\IOutOfOfficeData' => __DIR__ . '/../../..' . '/lib/public/User/IOutOfOfficeData.php', 'OCP\\Util' => __DIR__ . '/../../..' . '/lib/public/Util.php', 'OCP\\WorkflowEngine\\EntityContext\\IContextPortation' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IContextPortation.php', 'OCP\\WorkflowEngine\\EntityContext\\IDisplayName' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IDisplayName.php', @@ -1795,6 +1800,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Updater\\VersionCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/VersionCheck.php', 'OC\\UserStatus\\ISettableProvider' => __DIR__ . '/../../..' . '/lib/private/UserStatus/ISettableProvider.php', 'OC\\UserStatus\\Manager' => __DIR__ . '/../../..' . '/lib/private/UserStatus/Manager.php', + 'OC\\User\\AvailabilityCoordinator' => __DIR__ . '/../../..' . '/lib/private/User/AvailabilityCoordinator.php', 'OC\\User\\Backend' => __DIR__ . '/../../..' . '/lib/private/User/Backend.php', 'OC\\User\\Database' => __DIR__ . '/../../..' . '/lib/private/User/Database.php', 'OC\\User\\DisplayNameCache' => __DIR__ . '/../../..' . '/lib/private/User/DisplayNameCache.php', @@ -1804,6 +1810,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\User\\LoginException' => __DIR__ . '/../../..' . '/lib/private/User/LoginException.php', 'OC\\User\\Manager' => __DIR__ . '/../../..' . '/lib/private/User/Manager.php', 'OC\\User\\NoUserException' => __DIR__ . '/../../..' . '/lib/private/User/NoUserException.php', + 'OC\\User\\OutOfOfficeData' => __DIR__ . '/../../..' . '/lib/private/User/OutOfOfficeData.php', 'OC\\User\\Session' => __DIR__ . '/../../..' . '/lib/private/User/Session.php', 'OC\\User\\User' => __DIR__ . '/../../..' . '/lib/private/User/User.php', 'OC_API' => __DIR__ . '/../../..' . '/lib/private/legacy/OC_API.php', diff --git a/lib/private/User/AvailabilityCoordinator.php b/lib/private/User/AvailabilityCoordinator.php new file mode 100644 index 00000000000..fe0db92fd0f --- /dev/null +++ b/lib/private/User/AvailabilityCoordinator.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * @author Richard Steinmetz <richard@steinmetz.cloud> + * + * @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/>. + */ + +namespace OC\User; + +use JsonException; +use OCA\DAV\Db\AbsenceMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IUser; +use OCP\User\IAvailabilityCoordinator; +use OCP\User\IOutOfOfficeData; +use Psr\Log\LoggerInterface; + +class AvailabilityCoordinator implements IAvailabilityCoordinator { + private ICache $cache; + + public function __construct( + ICacheFactory $cacheFactory, + private AbsenceMapper $absenceMapper, + private LoggerInterface $logger, + ) { + $this->cache = $cacheFactory->createLocal('OutOfOfficeData'); + } + + private function getCachedOutOfOfficeData(IUser $user): ?OutOfOfficeData { + $cachedString = $this->cache->get($user->getUID()); + if ($cachedString === null) { + return null; + } + + try { + $cachedData = json_decode($cachedString, true, 10, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + $this->logger->error('Failed to deserialize cached out-of-office data: ' . $e->getMessage(), [ + 'exception' => $e, + 'json' => $cachedString, + ]); + return null; + } + + return new OutOfOfficeData( + $cachedData['id'], + $user, + $cachedData['startDate'], + $cachedData['endDate'], + $cachedData['shortMessage'], + $cachedData['message'], + ); + } + + private function setCachedOutOfOfficeData(IOutOfOfficeData $data): void { + try { + $cachedString = json_encode([ + 'id' => $data->getId(), + 'startDate' => $data->getStartDate(), + 'endDate' => $data->getEndDate(), + 'shortMessage' => $data->getShortMessage(), + 'message' => $data->getMessage(), + ], JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + $this->logger->error('Failed to serialize out-of-office data: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + return; + } + + $this->cache->set($data->getUser()->getUID(), $cachedString, 300); + } + + public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData { + $cachedData = $this->getCachedOutOfOfficeData($user); + if ($cachedData !== null) { + return $cachedData; + } + + try { + $absenceData = $this->absenceMapper->findByUserId($user->getUID()); + } catch (DoesNotExistException $e) { + return null; + } + + $data = $absenceData->toOutOufOfficeData($user); + $this->setCachedOutOfOfficeData($data); + return $data; + } +} diff --git a/lib/private/User/OutOfOfficeData.php b/lib/private/User/OutOfOfficeData.php new file mode 100644 index 00000000000..12b7e03a0ae --- /dev/null +++ b/lib/private/User/OutOfOfficeData.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OC\User; + +use OCP\IUser; +use OCP\User\IOutOfOfficeData; + +class OutOfOfficeData implements IOutOfOfficeData { + public function __construct(private string $id, + private IUser $user, + private int $startDate, + private int $endDate, + private string $shortMessage, + private string $message) { + } + + public function getId(): string { + return $this->id; + } + + public function getUser(): IUser { + return $this->user; + } + + public function getStartDate(): int { + return $this->startDate; + } + + public function getEndDate(): int { + return $this->endDate; + } + + public function getShortMessage(): string { + return $this->shortMessage; + } + + public function getMessage(): string { + return $this->message; + } +} diff --git a/lib/public/User/Events/OutOfOfficeChangedEvent.php b/lib/public/User/Events/OutOfOfficeChangedEvent.php new file mode 100644 index 00000000000..5e5753b7202 --- /dev/null +++ b/lib/public/User/Events/OutOfOfficeChangedEvent.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\User\Events; + +use OCP\EventDispatcher\Event; +use OCP\User\IOutOfOfficeData; + +/** + * Emitted when a user's out-of-office period has changed + * + * @since 28.0.0 + */ +class OutOfOfficeChangedEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct(private IOutOfOfficeData $data) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getData(): IOutOfOfficeData { + return $this->data; + } +} diff --git a/lib/public/User/Events/OutOfOfficeClearedEvent.php b/lib/public/User/Events/OutOfOfficeClearedEvent.php new file mode 100644 index 00000000000..48a77c77023 --- /dev/null +++ b/lib/public/User/Events/OutOfOfficeClearedEvent.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\User\Events; + +use OCP\EventDispatcher\Event; +use OCP\User\IOutOfOfficeData; + +/** + * Emitted when a user's out-of-office period is cleared + * + * @since 28.0.0 + */ +class OutOfOfficeClearedEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct(private IOutOfOfficeData $data) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getData(): IOutOfOfficeData { + return $this->data; + } +} diff --git a/lib/public/User/Events/OutOfOfficeScheduledEvent.php b/lib/public/User/Events/OutOfOfficeScheduledEvent.php new file mode 100644 index 00000000000..2bcbec63478 --- /dev/null +++ b/lib/public/User/Events/OutOfOfficeScheduledEvent.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\User\Events; + +use OCP\EventDispatcher\Event; +use OCP\User\IOutOfOfficeData; + +/** + * Emitted when a user's out-of-office period is scheduled + * + * @since 28.0.0 + */ +class OutOfOfficeScheduledEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct(private IOutOfOfficeData $data) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getData(): IOutOfOfficeData { + return $this->data; + } +} diff --git a/lib/public/User/IAvailabilityCoordinator.php b/lib/public/User/IAvailabilityCoordinator.php new file mode 100644 index 00000000000..113e3491714 --- /dev/null +++ b/lib/public/User/IAvailabilityCoordinator.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\User; + +use OCP\IUser; + +/** + * Coordinator for availability and out-of-office messages + * + * @since 28.0.0 + */ +interface IAvailabilityCoordinator { + /** + * Get the user's out-of-office message, if any + * + * @since 28.0.0 + */ + public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData; +} diff --git a/lib/public/User/IOutOfOfficeData.php b/lib/public/User/IOutOfOfficeData.php new file mode 100644 index 00000000000..03444449d58 --- /dev/null +++ b/lib/public/User/IOutOfOfficeData.php @@ -0,0 +1,77 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCP\User; + +use OCP\IUser; + +/** + * DTO to hold out-of-office information of a user + * + * @since 28.0.0 + */ +interface IOutOfOfficeData { + /** + * Get the unique token assigned to the current out-of-office event + * + * @since 28.0.0 + */ + public function getId(): string; + + /** + * @since 28.0.0 + */ + public function getUser(): IUser; + + /** + * Get the accurate out-of-office start date + * + * This event is not guaranteed to be emitted exactly at start date + * + * @since 28.0.0 + */ + public function getStartDate(): int; + + /** + * Get the (preliminary) out-of-office end date + * + * @since 28.0.0 + */ + public function getEndDate(): int; + + /** + * Get the short summary text displayed in the user status and similar + * + * @since 28.0.0 + */ + public function getShortMessage(): string; + + /** + * Get the long out-of-office message for auto responders and similar + * + * @since 28.0.0 + */ + public function getMessage(): string; +} diff --git a/tests/lib/User/AvailabilityCoordinatorTest.php b/tests/lib/User/AvailabilityCoordinatorTest.php new file mode 100644 index 00000000000..8e847f7e5d5 --- /dev/null +++ b/tests/lib/User/AvailabilityCoordinatorTest.php @@ -0,0 +1,164 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud> + * + * @author Richard Steinmetz <richard@steinmetz.cloud> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Test\User; + +use OC\User\AvailabilityCoordinator; +use OC\User\OutOfOfficeData; +use OCA\DAV\Db\Absence; +use OCA\DAV\Db\AbsenceMapper; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IUser; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class AvailabilityCoordinatorTest extends TestCase { + private AvailabilityCoordinator $availabilityCoordinator; + private ICacheFactory $cacheFactory; + private ICache $cache; + private AbsenceMapper $absenceMapper; + private LoggerInterface $logger; + + protected function setUp(): void { + parent::setUp(); + + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->cache = $this->createMock(ICache::class); + $this->absenceMapper = $this->createMock(AbsenceMapper::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->cacheFactory->expects(self::once()) + ->method('createLocal') + ->willReturn($this->cache); + + $this->availabilityCoordinator = new AvailabilityCoordinator( + $this->cacheFactory, + $this->absenceMapper, + $this->logger, + ); + } + + public function testGetOutOfOfficeData(): void { + $absence = new Absence(); + $absence->setId(420); + $absence->setUserId('user'); + $absence->setFirstDay('2023-10-01'); + $absence->setLastDay('2023-10-08'); + $absence->setStatus('Vacation'); + $absence->setMessage('On vacation'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->cache->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn(null); + $this->absenceMapper->expects(self::once()) + ->method('findByUserId') + ->with('user') + ->willReturn($absence); + $this->cache->expects(self::once()) + ->method('set') + ->with('user', '{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}', 300); + + $expected = new OutOfOfficeData( + '420', + $user, + 1696118400, + 1696723200, + 'Vacation', + 'On vacation', + ); + $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); + self::assertEquals($expected, $actual); + } + + public function testGetOutOfOfficeDataWithCachedData(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->cache->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn('{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}'); + $this->absenceMapper->expects(self::never()) + ->method('findByUserId'); + $this->cache->expects(self::never()) + ->method('set'); + + $expected = new OutOfOfficeData( + '420', + $user, + 1696118400, + 1696723200, + 'Vacation', + 'On vacation', + ); + $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); + self::assertEquals($expected, $actual); + } + + public function testGetOutOfOfficeDataWithInvalidCachedData(): void { + $absence = new Absence(); + $absence->setId(420); + $absence->setUserId('user'); + $absence->setFirstDay('2023-10-01'); + $absence->setLastDay('2023-10-08'); + $absence->setStatus('Vacation'); + $absence->setMessage('On vacation'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->cache->expects(self::once()) + ->method('get') + ->with('user') + ->willReturn('{"id":"420",}'); + $this->absenceMapper->expects(self::once()) + ->method('findByUserId') + ->with('user') + ->willReturn($absence); + $this->cache->expects(self::once()) + ->method('set') + ->with('user', '{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}', 300); + + $expected = new OutOfOfficeData( + '420', + $user, + 1696118400, + 1696723200, + 'Vacation', + 'On vacation', + ); + $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); + self::assertEquals($expected, $actual); + } +} |