From 7cb65d8e43b6d8058f51d870d39f6ce7539a553e Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 2 Apr 2024 14:00:58 +0200 Subject: [PATCH] feat(Context): add share logic for contexts Signed-off-by: Arthur Schiwon --- appinfo/routes.php | 2 + lib/Controller/Api1Controller.php | 68 +++++++++++- lib/Controller/ShareController.php | 36 +++++- lib/Db/ContextNavigation.php | 35 ++++++ lib/Db/ContextNavigationMapper.php | 41 +++++++ lib/ResponseDefinitions.php | 7 ++ lib/Service/PermissionsService.php | 12 ++ lib/Service/ShareService.php | 75 ++++++++++++- openapi.json | 171 +++++++++++++++++++++++++++++ src/types/openapi/openapi.ts | 69 ++++++++++++ 10 files changed, 506 insertions(+), 10 deletions(-) create mode 100644 lib/Db/ContextNavigation.php create mode 100644 lib/Db/ContextNavigationMapper.php diff --git a/appinfo/routes.php b/appinfo/routes.php index b3d285cf7..38bfd10d3 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -33,6 +33,7 @@ ['name' => 'api1#createShare', 'url' => '/api/1/shares', 'verb' => 'POST'], ['name' => 'api1#deleteShare', 'url' => '/api/1/shares/{shareId}', 'verb' => 'DELETE'], ['name' => 'api1#updateSharePermissions', 'url' => '/api/1/shares/{shareId}', 'verb' => 'PUT'], + ['name' => 'api1#updateShareDisplayMode', 'url' => '/api/1/shares/{shareId}/display-mode', 'verb' => 'PUT'], ['name' => 'api1#createTableShare', 'url' => '/api/1/tables/{tableId}/shares', 'verb' => 'POST'], // -> columns ['name' => 'api1#indexTableColumns', 'url' => '/api/1/tables/{tableId}/columns', 'verb' => 'GET'], @@ -97,6 +98,7 @@ ['name' => 'share#show', 'url' => '/share/{id}', 'verb' => 'GET'], ['name' => 'share#create', 'url' => '/share', 'verb' => 'POST'], ['name' => 'share#updatePermission', 'url' => '/share/{id}/permission', 'verb' => 'PUT'], + ['name' => 'share#updateDisplayMode', 'url' => '/share/{id}/display-mode', 'verb' => 'PUT'], ['name' => 'share#destroy', 'url' => '/share/{id}', 'verb' => 'DELETE'], // import diff --git a/lib/Controller/Api1Controller.php b/lib/Controller/Api1Controller.php index f9614349b..60ad776b0 100644 --- a/lib/Controller/Api1Controller.php +++ b/lib/Controller/Api1Controller.php @@ -34,6 +34,7 @@ * @psalm-import-type TablesColumn from ResponseDefinitions * @psalm-import-type TablesRow from ResponseDefinitions * @psalm-import-type TablesImportState from ResponseDefinitions + * @psalm-import-type TablesContextNavigation from ResponseDefinitions */ class Api1Controller extends ApiController { private TableService $tableService; @@ -483,15 +484,27 @@ public function indexTableShares(int $tableId): DataResponse { * @param bool $permissionUpdate Permission if receiver can update data * @param bool $permissionDelete Permission if receiver can delete data * @param bool $permissionManage Permission if receiver can manage node + * @param int $displayMode context shares only, whether it should appear in nav bar. 0: no, 1: recipients, 2: all * @return DataResponse|DataResponse * * 200: Share returned * 403: No permissions * 404: Not found */ - public function createShare(int $nodeId, string $nodeType, string $receiver, string $receiverType, bool $permissionRead = false, bool $permissionCreate = false, bool $permissionUpdate = false, bool $permissionDelete = false, bool $permissionManage = false): DataResponse { + public function createShare( + int $nodeId, + string $nodeType, + string $receiver, + string $receiverType, + bool $permissionRead = false, + bool $permissionCreate = false, + bool $permissionUpdate = false, + bool $permissionDelete = false, + bool $permissionManage = false, + int $displayMode = 0, + ): DataResponse { try { - return new DataResponse($this->shareService->create($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage)->jsonSerialize()); + return new DataResponse($this->shareService->create($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage, $displayMode)->jsonSerialize()); } catch (PermissionError $e) { $this->logger->warning('A permission error occurred: '.$e->getMessage()); $message = ['message' => $e->getMessage()]; @@ -573,6 +586,55 @@ public function updateSharePermissions(int $shareId, string $permissionType, boo } } + /** + * Updates the display mode of a context share + * + * @NoAdminRequired + * @CORS + * @NoCSRFRequired + * + * @param int $shareId Share ID + * @param int $displayMode The new value for the display mode of the nav bar icon. 0: hidden, 1: visible for recipients, 2: visible for all + * @param string $target "default" to set the default, "self" to set an override for the authenticated user + * @return DataResponse|DataResponse + * + * 200: Display mode updated + * 400: Invalid parameter + * 403: No permissions + * 404: Share not found + * + * @psalm-param int<0, 2> $displayMode + * @psalm-param ("default"|"self") $target + */ + public function updateShareDisplayMode(int $shareId, int $displayMode, string $target = 'default'): DataResponse { + if ($target === 'default') { + $userId = ''; + } elseif ($target === 'self') { + $userId = $this->userId; + } else { + $error = 'target parameter must be either "default" or "self"'; + $this->logger->warning(sprintf('An internal error or exception occurred: %s', $error)); + $message = ['message' => $error]; + return new DataResponse($message, Http::STATUS_BAD_REQUEST); + } + + try { + return new DataResponse($this->shareService->updateDisplayMode($shareId, $displayMode, $userId)->jsonSerialize()); + } catch (InternalError $e) { + $this->logger->warning('An internal error or exception occurred: '.$e->getMessage()); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (NotFoundError $e) { + $this->logger->warning('A not found error occurred: ' . $e->getMessage()); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_NOT_FOUND); + } catch (PermissionError $e) { + $this->logger->warning('A permission error occurred: '.$e->getMessage()); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_FORBIDDEN); + } + } + // Columns /** @@ -1303,7 +1365,7 @@ public function importInView(int $viewId, string $path, bool $createMissingColum */ public function createTableShare(int $tableId, string $receiver, string $receiverType, bool $permissionRead, bool $permissionCreate, bool $permissionUpdate, bool $permissionDelete, bool $permissionManage): DataResponse { try { - return new DataResponse($this->shareService->create($tableId, 'table', $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage)->jsonSerialize()); + return new DataResponse($this->shareService->create($tableId, 'table', $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage, 0)->jsonSerialize()); } catch (PermissionError $e) { $this->logger->warning('A permission error occurred: '.$e->getMessage()); $message = ['message' => $e->getMessage()]; diff --git a/lib/Controller/ShareController.php b/lib/Controller/ShareController.php index 26a194af1..16c761e69 100644 --- a/lib/Controller/ShareController.php +++ b/lib/Controller/ShareController.php @@ -61,9 +61,20 @@ public function show(int $id): DataResponse { /** * @NoAdminRequired */ - public function create(int $nodeId, string $nodeType, string $receiver, string $receiverType, bool $permissionRead = false, bool $permissionCreate = false, bool $permissionUpdate = false, bool $permissionDelete = false, bool $permissionManage = false): DataResponse { - return $this->handleError(function () use ($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage) { - return $this->service->create($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage); + public function create( + int $nodeId, + string $nodeType, + string $receiver, + string $receiverType, + bool $permissionRead = false, + bool $permissionCreate = false, + bool $permissionUpdate = false, + bool $permissionDelete = false, + bool $permissionManage = false, + int $displayMode = 0, + ): DataResponse { + return $this->handleError(function () use ($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage, $displayMode) { + return $this->service->create($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage, $displayMode); }); } @@ -76,6 +87,25 @@ public function updatePermission(int $id, string $permission, bool $value): Data }); } + /** + * @NoAdminRequired + * @psalm-param int<0, 2> $displayMode + * @psalm-param ("default"|"self") $target + */ + public function updateDisplayMode(int $id, int $displayMode, string $target = 'default') { + return $this->handleError(function () use ($id, $displayMode, $target) { + if ($target === 'default') { + $userId = ''; + } elseif ($target === 'self') { + $userId = $this->userId; + } else { + throw new \InvalidArgumentException('target parameter must be either "default" or "self"'); + } + + return $this->service->updateDisplayMode($id, $displayMode, $userId); + }); + } + /** * @NoAdminRequired */ diff --git a/lib/Db/ContextNavigation.php b/lib/Db/ContextNavigation.php new file mode 100644 index 000000000..14702ace2 --- /dev/null +++ b/lib/Db/ContextNavigation.php @@ -0,0 +1,35 @@ +addType('shareId', 'integer'); + $this->addType('displayMode', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'shareId' => $this->getShareId(), + 'displayMode' => $this->getDisplayMode(), + 'userId' => $this->getUserId(), + ]; + } +} diff --git a/lib/Db/ContextNavigationMapper.php b/lib/Db/ContextNavigationMapper.php new file mode 100644 index 000000000..ca907f96b --- /dev/null +++ b/lib/Db/ContextNavigationMapper.php @@ -0,0 +1,41 @@ + */ +class ContextNavigationMapper extends QBMapper { + protected string $table = 'tables_contexts_navigation'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, ContextNavigation::class); + } + + /** + * @throws Exception + */ + public function deleteByShareId(int $shareId): int { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->table) + ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT))); + return $qb->executeStatement(); + } + + /** + * @throws Exception + */ + public function setDisplayModeByShareId(int $shareId, int $displayMode, string $userId): ContextNavigation { + $entity = new ContextNavigation(); + $entity->setShareId($shareId); + $entity->setDisplayMode($displayMode); + $entity->setUserId($userId); + + return $this->insertOrUpdate($entity); + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index d738a3c0d..554c14940 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -137,6 +137,13 @@ * owner: string, * ownerType: int, * } + * + * @psalm-type TablesContextNavigation = array{ + * id: int, + * shareId: int, + * displayMode: int, + * userId: string, + * } */ class ResponseDefinitions { } diff --git a/lib/Service/PermissionsService.php b/lib/Service/PermissionsService.php index 922bd4685..81d9a6f74 100644 --- a/lib/Service/PermissionsService.php +++ b/lib/Service/PermissionsService.php @@ -152,6 +152,18 @@ public function canManageContextById(int $contextId, ?string $userId = null): bo return $context->getOwnerId() === $userId; } + /** + * @throws Exception + */ + public function canAccessContextById(int $contextId, ?string $userId = null): bool { + try { + $this->contextMapper->findById($contextId, $userId ?? $this->userId); + return true; + } catch (NotFoundError $e) { + return false; + } + } + public function canAccessView(View $view, ?string $userId = null): bool { if($this->basisCheck($view, 'view', $userId)) { return true; diff --git a/lib/Service/ShareService.php b/lib/Service/ShareService.php index 1fdf818ca..792c4c33c 100644 --- a/lib/Service/ShareService.php +++ b/lib/Service/ShareService.php @@ -6,6 +6,9 @@ use DateTime; +use InvalidArgumentException; +use OCA\Tables\Db\ContextNavigation; +use OCA\Tables\Db\ContextNavigationMapper; use OCA\Tables\Db\Share; use OCA\Tables\Db\ShareMapper; use OCA\Tables\Db\Table; @@ -37,15 +40,26 @@ class ShareService extends SuperService { protected UserHelper $userHelper; protected GroupHelper $groupHelper; - - public function __construct(PermissionsService $permissionsService, LoggerInterface $logger, ?string $userId, - ShareMapper $shareMapper, TableMapper $tableMapper, ViewMapper $viewMapper, UserHelper $userHelper, GroupHelper $groupHelper) { + private ContextNavigationMapper $contextNavigationMapper; + + public function __construct( + PermissionsService $permissionsService, + LoggerInterface $logger, + ?string $userId, + ShareMapper $shareMapper, + TableMapper $tableMapper, + ViewMapper $viewMapper, + UserHelper $userHelper, + GroupHelper $groupHelper, + ContextNavigationMapper $contextNavigationMapper, + ) { parent::__construct($logger, $userId, $permissionsService); $this->mapper = $shareMapper; $this->tableMapper = $tableMapper; $this->viewMapper = $viewMapper; $this->userHelper = $userHelper; $this->groupHelper = $groupHelper; + $this->contextNavigationMapper = $contextNavigationMapper; } @@ -190,7 +204,7 @@ public function getSharedPermissionsIfSharedWithMe(int $elementId, string $eleme * @return Share * @throws InternalError */ - public function create(int $nodeId, string $nodeType, string $receiver, string $receiverType, bool $permissionRead, bool $permissionCreate, bool $permissionUpdate, bool $permissionDelete, bool $permissionManage):Share { + public function create(int $nodeId, string $nodeType, string $receiver, string $receiverType, bool $permissionRead, bool $permissionCreate, bool $permissionUpdate, bool $permissionDelete, bool $permissionManage, int $displayMode):Share { if (!$this->userId) { $e = new \Exception('No user given.'); $this->logger->error($e->getMessage(), ['exception' => $e]); @@ -219,6 +233,22 @@ public function create(int $nodeId, string $nodeType, string $receiver, string $ $this->logger->error($e->getMessage()); throw new InternalError($e->getMessage()); } + + if ($nodeType === 'context') { + // set the default visibility of the nav bar item for Application shares + $navigationItem = new ContextNavigation(); + $navigationItem->setShareId($item->getId()); + $navigationItem->setUserId(''); + $navigationItem->setDisplayMode($displayMode); + + try { + $this->contextNavigationMapper->insert($navigationItem); + } catch (Exception $e) { + $this->logger->error($e->getMessage()); + throw new InternalError($e->getMessage()); + } + } + return $this->addReceiverDisplayName($newShare); } @@ -280,6 +310,40 @@ public function updatePermission(int $id, string $permission, bool $value): Shar return $this->addReceiverDisplayName($share); } + /** + * @throws InternalError|PermissionError|NotFoundError + */ + public function updateDisplayMode(int $shareId, int $displayMode, string $userId): ContextNavigation { + try { + $item = $this->mapper->find($shareId); + + if ($item->getNodeType() !== 'context') { + // Contexts-only property + throw new InvalidArgumentException(get_class($this) . ' - ' . __FUNCTION__ . ': nav bar display mode can be set for shared contexts only'); + } + + if ($userId === '') { + // setting default display mode requires manage permissions + if (!$this->permissionsService->canManageContextById($item->getNodeId())) { + throw new PermissionError(sprintf('PermissionError: can not update share with id %d', $shareId)); + } + } else { + // setting user display mode override only requires access + if (!$this->permissionsService->canAccessContextById($item->getId())) { + throw new PermissionError(sprintf('PermissionError: can not update share with id %d', $shareId)); + } + } + + return $this->contextNavigationMapper->setDisplayModeByShareId($shareId, $displayMode, ''); + } catch (DoesNotExistException $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } catch (Exception|MultipleObjectsReturnedException $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); + } + } + /** * @param int $id * @return Share @@ -305,6 +369,9 @@ public function delete(int $id): Share { try { $this->mapper->delete($item); + if ($item->getNodeType() === 'context') { + $this->contextNavigationMapper->deleteByShareId($item->getId()); + } } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); diff --git a/openapi.json b/openapi.json index 5a96f7ff9..4305b078e 100644 --- a/openapi.json +++ b/openapi.json @@ -208,6 +208,32 @@ } } }, + "ContextNavigation": { + "type": "object", + "required": [ + "id", + "shareId", + "displayMode", + "userId" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "shareId": { + "type": "integer", + "format": "int64" + }, + "displayMode": { + "type": "integer", + "format": "int64" + }, + "userId": { + "type": "string" + } + } + }, "ImportState": { "type": "object", "required": [ @@ -2434,6 +2460,16 @@ 1 ] } + }, + { + "name": "displayMode", + "in": "query", + "description": "context shares only, whether it should appear in nav bar. 0: no, 1: recipients, 2: all", + "schema": { + "type": "integer", + "format": "int64", + "default": 0 + } } ], "responses": { @@ -2504,6 +2540,141 @@ } } }, + "/index.php/apps/tables/api/1/shares/{shareId}/display-mode": { + "put": { + "operationId": "api1-update-share-display-mode", + "summary": "Updates the display mode of a context share", + "tags": [ + "api1" + ], + "security": [ + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "displayMode", + "in": "query", + "description": "The new value for the display mode of the nav bar icon. 0: hidden, 1: visible for recipients, 2: visible for all", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0, + "maximum": 2 + } + }, + { + "name": "target", + "in": "query", + "description": "\"default\" to set the default, \"self\" to set an override for the authenticated user", + "schema": { + "type": "string", + "default": "default", + "enum": [ + "default", + "self" + ] + } + }, + { + "name": "shareId", + "in": "path", + "description": "Share ID", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Display mode updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextNavigation" + } + } + } + }, + "400": { + "description": "Invalid parameter", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "403": { + "description": "No permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, + "404": { + "description": "Share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, "/index.php/apps/tables/api/1/tables/{tableId}/columns": { "get": { "operationId": "api1-list-table-columns", diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 236c7d17d..5e11f19b5 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -60,6 +60,10 @@ export type paths = { /** Create a new share */ post: operations["api1-create-share"]; }; + "/index.php/apps/tables/api/1/shares/{shareId}/display-mode": { + /** Updates the display mode of a context share */ + put: operations["api1-update-share-display-mode"]; + }; "/index.php/apps/tables/api/1/tables/{tableId}/columns": { /** Get all columns for a table or a underlying view Return an empty array if no columns were found */ get: operations["api1-list-table-columns"]; @@ -283,6 +287,15 @@ export type components = { /** Format: int64 */ ownerType: number; }; + ContextNavigation: { + /** Format: int64 */ + id: number; + /** Format: int64 */ + shareId: number; + /** Format: int64 */ + displayMode: number; + userId: string; + }; ImportState: { /** Format: int64 */ found_columns_count: number; @@ -1079,6 +1092,8 @@ export type operations = { permissionDelete?: 0 | 1; /** @description Permission if receiver can manage node */ permissionManage?: 0 | 1; + /** @description context shares only, whether it should appear in nav bar. 0: no, 1: recipients, 2: all */ + displayMode?: number; }; }; responses: { @@ -1113,6 +1128,60 @@ export type operations = { }; }; }; + /** Updates the display mode of a context share */ + "api1-update-share-display-mode": { + parameters: { + query: { + /** @description The new value for the display mode of the nav bar icon. 0: hidden, 1: visible for recipients, 2: visible for all */ + displayMode: number; + /** @description "default" to set the default, "self" to set an override for the authenticated user */ + target?: "default" | "self"; + }; + path: { + /** @description Share ID */ + shareId: number; + }; + }; + responses: { + /** @description Display mode updated */ + 200: { + content: { + "application/json": components["schemas"]["ContextNavigation"]; + }; + }; + /** @description Invalid parameter */ + 400: { + content: { + "application/json": { + message: string; + }; + }; + }; + /** @description No permissions */ + 403: { + content: { + "application/json": { + message: string; + }; + }; + }; + /** @description Share not found */ + 404: { + content: { + "application/json": { + message: string; + }; + }; + }; + 500: { + content: { + "application/json": { + message: string; + }; + }; + }; + }; + }; /** Get all columns for a table or a underlying view Return an empty array if no columns were found */ "api1-list-table-columns": { parameters: {