diff --git a/appinfo/info.xml b/appinfo/info.xml index e6f1c544c..53672fbe8 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -56,6 +56,8 @@ Have a good time and manage whatever you want. OCA\Tables\Command\RemoveTable OCA\Tables\Command\RenameTable OCA\Tables\Command\ChangeOwnershipTable + OCA\Tables\Command\ListContexts + OCA\Tables\Command\ShowContext OCA\Tables\Command\Clean OCA\Tables\Command\CleanLegacy OCA\Tables\Command\TransferLegacyRows diff --git a/appinfo/routes.php b/appinfo/routes.php index f3c58beb5..da22566e5 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -128,5 +128,13 @@ ['name' => 'ApiFavorite#create', 'url' => '/api/2/favorites/{nodeType}/{nodeId}', 'verb' => 'POST', 'requirements' => ['nodeType' => '(\d+)', 'nodeId' => '(\d+)']], ['name' => 'ApiFavorite#destroy', 'url' => '/api/2/favorites/{nodeType}/{nodeId}', 'verb' => 'DELETE', 'requirements' => ['nodeType' => '(\d+)', 'nodeId' => '(\d+)']], + ['name' => 'Context#index', 'url' => '/api/2/contexts', 'verb' => 'GET'], + ['name' => 'Context#show', 'url' => '/api/2/contexts/{contextId}', 'verb' => 'GET'], + ['name' => 'Context#create', 'url' => '/api/2/contexts', 'verb' => 'POST'], + ['name' => 'Context#update', 'url' => '/api/2/contexts/{contextId}', 'verb' => 'PUT'], + ['name' => 'Context#transfer', 'url' => '/api/2/contexts/{contextId}/transfer', 'verb' => 'PUT'], + ['name' => 'Context#addNode', 'url' => '/api/2/contexts/{contextId}/nodes', 'verb' => 'POST'], + ['name' => 'Context#removeNode', 'url' => '/api/2/contexts/{contextId}/nodes/{nodeRelId}', 'verb' => 'DELETE'], + ['name' => 'Context#updateContentOrder', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}', 'verb' => 'PUT'], ] ]; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 738534a66..c5d12b859 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -9,6 +9,7 @@ use OCA\Tables\Listener\LoadAdditionalListener; use OCA\Tables\Listener\TablesReferenceListener; use OCA\Tables\Listener\UserDeletedListener; +use OCA\Tables\Middleware\PermissionMiddleware; use OCA\Tables\Reference\ContentReferenceProvider; use OCA\Tables\Reference\LegacyReferenceProvider; use OCA\Tables\Reference\ReferenceProvider; @@ -32,6 +33,8 @@ class Application extends App implements IBootstrap { public const NODE_TYPE_TABLE = 0; public const NODE_TYPE_VIEW = 1; + public const OWNER_TYPE_USER = 0; + public function __construct() { parent::__construct(self::APP_ID); } @@ -65,6 +68,8 @@ public function register(IRegistrationContext $context): void { } $context->registerCapability(Capabilities::class); + + $context->registerMiddleware(PermissionMiddleware::class); } public function boot(IBootContext $context): void { diff --git a/lib/Command/ListContexts.php b/lib/Command/ListContexts.php new file mode 100644 index 000000000..3af8e1b52 --- /dev/null +++ b/lib/Command/ListContexts.php @@ -0,0 +1,89 @@ +contextService = $contextService; + $this->logger = $logger; + $this->config = $config; + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('tables:contexts:list') + ->setDescription('Get all contexts or contexts available to a specified user') + ->addArgument( + 'user-id', + InputArgument::OPTIONAL, + 'User ID of the user' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $userId = trim($input->getArgument('user-id')); + if ($userId === '') { + $userId = null; + } + + try { + $contexts = $this->contextService->findAll($userId); + } catch (InternalError|Exception $e) { + $output->writeln('Error while reading contexts from DB.'); + $this->logger->warning('Following error occurred during executing occ command "{class}"', + [ + 'app' => 'tables', + 'class' => self::class, + 'exception' => $e, + ] + ); + if ($this->config->getSystemValueBool('debug', false)) { + $output->writeln(sprintf('%s', $e->getMessage())); + $output->writeln(''); + debug_print_backtrace(); + $output->writeln(''); + } + return 1; + } + + foreach ($contexts as $context) { + $contextArray = json_decode(json_encode($context), true); + + $contextArray['ownerType'] = match ($contextArray['ownerType']) { + 1 => 'group', + default => 'user', + }; + + $out = ['ID ' . $contextArray['id'] => $contextArray]; + unset($out[$contextArray['id']]['id']); + $this->writeArrayInOutputFormat($input, $output, $out); + } + + return 0; + } +} diff --git a/lib/Command/ShowContext.php b/lib/Command/ShowContext.php new file mode 100644 index 000000000..239483c51 --- /dev/null +++ b/lib/Command/ShowContext.php @@ -0,0 +1,98 @@ +contextService = $contextService; + $this->logger = $logger; + $this->config = $config; + } + + protected function configure(): void { + parent::configure(); + $this + ->setName('tables:contexts:show') + ->setDescription('Get all contexts or contexts available to a specified user') + ->addArgument( + 'context-id', + InputArgument::REQUIRED, + 'The ID of the context to show' + ) + ->addArgument( + 'user-id', + InputArgument::OPTIONAL, + 'Optionally, showing the context from the perspective of the given user' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $contextId = trim($input->getArgument('context-id')); + if ($contextId === '' || !is_numeric($contextId)) { + $output->writeln('Invalid Context ID'); + return 1; + } + + $userId = trim($input->getArgument('user-id')); + if ($userId === '') { + $userId = null; + } + + try { + $context = $this->contextService->findById($contextId, $userId); + } catch (InternalError|Exception $e) { + $output->writeln('Error while reading contexts from DB.'); + $this->logger->warning('Following error occurred during executing occ command "{class}"', + [ + 'app' => 'tables', + 'class' => self::class, + 'exception' => $e, + ] + ); + if ($this->config->getSystemValueBool('debug', false)) { + $output->writeln(sprintf('%s', $e->getMessage())); + $output->writeln(''); + debug_print_backtrace(); + $output->writeln(''); + } + return 1; + } + + $contextArray = json_decode(json_encode($context), true); + + $contextArray['ownerType'] = match ($contextArray['ownerType']) { + 1 => 'group', + default => 'user', + }; + + $out = ['ID ' . $contextArray['id'] => $contextArray]; + unset($out[$contextArray['id']]['id']); + $this->writeArrayInOutputFormat($input, $output, $out); + + return 0; + } +} diff --git a/lib/Controller/AOCSController.php b/lib/Controller/AOCSController.php index 8308a615e..54cd1411f 100644 --- a/lib/Controller/AOCSController.php +++ b/lib/Controller/AOCSController.php @@ -4,6 +4,7 @@ use Exception; use OCA\Tables\AppInfo\Application; +use OCA\Tables\Errors\BadRequestError; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; @@ -59,4 +60,13 @@ protected function handleNotFoundError(NotFoundError $e): DataResponse { return new DataResponse(['message' => $this->n->t('A not found error occurred. More details can be found in the logs. Please reach out to your administration.')], Http::STATUS_NOT_FOUND); } + /** + * @param BadRequestError $e + * @return DataResponse + */ + protected function handleBadRequestError(BadRequestError $e): DataResponse { + $this->logger->warning('An bad request was encountered: ['. $e->getCode() . ']' . $e->getMessage()); + return new DataResponse(['message' => $this->n->t('An error caused by an invalid request occurred. More details can be found in the logs. Please reach out to your administration.')], Http::STATUS_BAD_REQUEST); + } + } diff --git a/lib/Controller/ContextController.php b/lib/Controller/ContextController.php new file mode 100644 index 000000000..bd71417a1 --- /dev/null +++ b/lib/Controller/ContextController.php @@ -0,0 +1,285 @@ +contextService = $contextService; + $this->userId = $userId; + } + + /** + * [api v3] Get all contexts available to the requesting person + * + * Return an empty array if no contexts were found + * + * @return DataResponse|DataResponse + * + * 200: reporting in available contexts + * + * @NoAdminRequired + */ + public function index(): DataResponse { + try { + $contexts = $this->contextService->findAll($this->userId); + return new DataResponse($this->contextsToArray($contexts)); + } catch (InternalError|Exception $e) { + return $this->handleError($e); + } + } + + /** + * [api v2] Get information about the requests context + * + * @param int $contextId ID of the context + * @return DataResponse|DataResponse + * + * 200: returning the full context information + * 404: context not found or not available anymore + * + * @NoAdminRequired + */ + public function show(int $contextId): DataResponse { + try { + $context = $this->contextService->findById($contextId, $this->userId); + return new DataResponse($context->jsonSerialize()); + } catch (NotFoundError $e) { + return $this->handleNotFoundError($e); + } catch (InternalError|Exception $e) { + return $this->handleError($e); + } + } + + /** + * [api v2] Create a new context and return it + * + * @NoAdminRequired + * + * @param string $name Name of the context + * @param string $iconName Material design icon name of the context + * @param string $description Descriptive text of the context + * @param array{id: int, type: int, permissions: int}|array $nodes optional nodes to be connected to this context + * + * @return DataResponse|DataResponse + * + * 200: returning the full context information + */ + public function create(string $name, string $iconName, string $description = '', array $nodes = []): DataResponse { + try { + return new DataResponse($this->contextService->create($name, $iconName, $description, $nodes, $this->userId, 0)->jsonSerialize()); + } catch (Exception $e) { + return $this->handleError($e); + } + } + + /** + * [api v2] Update an existing context and return it + * + * @param int $contextId ID of the context + * @param ?string $name provide this parameter to set a new name + * @param ?string $iconName provide this parameter to set a new icon + * @param ?string $description provide this parameter to set a new description + * @param ?array{id: int, type: int, permissions: int, order: int} $nodes provide this parameter to set a new list of nodes. + * + * @return DataResponse|DataResponse + * + * 200: returning the full context information + * 403: No permissions + * 404: Not found + * + * @NoAdminRequired + * @CanManageContext + */ + public function update(int $contextId, ?string $name, ?string $iconName, ?string $description, ?array $nodes): DataResponse { + try { + return new DataResponse($this->contextService->update( + $contextId, + $this->userId, + $name, + $iconName, + $description, + $nodes, + )->jsonSerialize()); + } catch (Exception|MultipleObjectsReturnedException $e) { + return $this->handleError($e); + } catch (DoesNotExistException $e) { + return $this->handleNotFoundError(new NotFoundError($e->getMessage(), $e->getCode(), $e)); + } + } + + /** + * [api v2] Transfer the ownership of a context and return it + * + * @param int $contextId ID of the context + * @param string $newOwnerId ID of the new owner + * @param int $newOwnerType any Application::OWNER_TYPE_* constant + * + * @return DataResponse|DataResponse + * + * 200: Ownership transferred + * 400: Invalid request + * 403: No permissions + * 404: Not found + * + * @NoAdminRequired + * @CanManageContext + * + * @psalm-param int<0, max> $contextId + * @psalm-param int<0, 0> $newOwnerType + */ + public function transfer(int $contextId, string $newOwnerId, int $newOwnerType = 0): DataResponse { + try { + return new DataResponse($this->contextService->transfer($contextId, $newOwnerId, $newOwnerType)->jsonSerialize()); + } catch (Exception|MultipleObjectsReturnedException $e) { + return $this->handleError($e); + } catch (DoesNotExistException $e) { + return $this->handleNotFoundError(new NotFoundError($e->getMessage(), $e->getCode(), $e)); + } catch (BadRequestError $e) { + return $this->handleBadRequestError($e); + } + } + + /** + * [api v2] Add a node to a Context + * + * @param int $contextId ID of the context + * @param int $nodeId ID of the node + * @param int $nodeType any Application::NODE_TYPE_* constant + * @param int $permissions bitmask of the permissions for context recipients + * @param ?int $order in which order the node should appear within the context + * + * @return DataResponse|DataResponse + * + * 200: Node added successfully + * 403: No permissions + * 404: Not found + * + * @NoAdminRequired + * @CanManageNode + */ + public function addNode(int $contextId, int $nodeId, int $nodeType, int $permissions, ?int $order = null): DataResponse { + try { + $rel = $this->contextService->addNodeToContextById($contextId, $nodeId, $nodeType, $permissions, $this->userId); + $this->contextService->addNodeRelToPage($rel, $order); + $context = $this->contextService->findById($rel->getContextId(), $this->userId); + return new DataResponse($context->jsonSerialize()); + } catch (NotFoundError $e) { + return $this->handleNotFoundError($e); + } catch (DoesNotExistException $e) { + return $this->handleNotFoundError(new NotFoundError($e->getMessage(), $e->getCode(), $e)); + } catch (MultipleObjectsReturnedException|Exception|InternalError $e) { + return $this->handleError($e); + } + } + + /** + * [api v2] Remove a node from a Context + * + * @param int $contextId ID of the context + * @param int $nodeRelId ID of the node-in-context relation + * + * @return DataResponse|DataResponse + * + * 200: Node removed successfully + * 400: Invalid request + * 403: No permissions + * 404: Not found + * + * @NoAdminRequired + * @CanManageContext + */ + public function removeNode(int $contextId, int $nodeRelId): DataResponse { + // we could do without the contextId, however it is used by the Permission Middleware + // and also results in a more consistent endpoint url + try { + $context = $this->contextService->findById($contextId, $this->userId); + if (!isset($context->getNodes()[$nodeRelId])) { + return $this->handleBadRequestError(new BadRequestError('Node Relation ID not found in given Context')); + } + $nodeRelation = $this->contextService->removeNodeFromContextById($nodeRelId); + $this->contextService->removeNodeRelFromAllPages($nodeRelation); + $context = $this->contextService->findById($contextId, $this->userId); + return new DataResponse($context->jsonSerialize()); + } catch (NotFoundError $e) { + return $this->handleNotFoundError($e); + } catch (DoesNotExistException $e) { + return $this->handleNotFoundError(new NotFoundError($e->getMessage(), $e->getCode(), $e)); + } catch (MultipleObjectsReturnedException|Exception|InternalError $e) { + return $this->handleError($e); + } + } + + /** + * [api v2] Update the order on a page of a context + * + * @param int $contextId ID of the context + * @param int $pageId ID of the page + * @param array{id: int, order: int} $content content items with it and order values + * + * @return DataResponse|DataResponse + * + * @NoAdminRequired + * @CanManageContext + * + * 200: content updated successfully + * 400: Invalid request + * 403: No permissions + * 404: Not found + */ + public function updateContentOrder(int $contextId, int $pageId, array $content): DataResponse { + try { + $context = $this->contextService->findById($contextId, $this->userId); + } catch (Exception|InternalError $e) { + return $this->handleError($e); + } catch (NotFoundError $e) { + return $this->handleNotFoundError($e); + } + if (!isset($context->getPages()[$pageId])) { + return $this->handleBadRequestError(new BadRequestError('Page not found in given Context')); + } + + return new DataResponse($this->contextService->updateContentOrder($pageId, $content)); + } + + /** + * @param Context[] $contexts + * @return array + */ + protected function contextsToArray(array $contexts): array { + $result = []; + foreach ($contexts as $context) { + $result[] = $context->jsonSerialize(); + } + return $result; + } +} diff --git a/lib/Db/Context.php b/lib/Db/Context.php new file mode 100644 index 000000000..8da45c6b3 --- /dev/null +++ b/lib/Db/Context.php @@ -0,0 +1,63 @@ +addType('id', 'integer'); + } + + public function jsonSerialize(): array { + // basic information + $data = [ + 'id' => $this->getId(), + 'name' => $this->getName(), + 'iconName' => $this->getIcon(), + 'description' => $this->getDescription(), + 'owner' => $this->getOwnerId(), + 'ownerType' => $this->getOwnerType() + ]; + + // extended data + if (is_array($this->sharing) || is_array($this->nodes) || is_array($this->pages)) { + $data['sharing'] = $this->getSharing(); + $data['nodes'] = $this->getNodes(); + $data['pages'] = $this->getPages(); + } + + return $data; + } +} diff --git a/lib/Db/ContextMapper.php b/lib/Db/ContextMapper.php new file mode 100644 index 000000000..702af3767 --- /dev/null +++ b/lib/Db/ContextMapper.php @@ -0,0 +1,219 @@ + */ +class ContextMapper extends QBMapper { + protected string $table = 'tables_contexts_context'; + private UserHelper $userHelper; + + public function __construct(IDBConnection $db, UserHelper $userHelper) { + $this->userHelper = $userHelper; + parent::__construct($db, $this->table, Context::class); + } + + protected function getFindContextBaseQuery(?string $userId): IQueryBuilder { + $qb = $this->db->getQueryBuilder(); + + $qb->select( + 'c.*', + 'r.id as node_rel_id', 'r.node_id', 'r.node_type', 'r.permissions', + 'p.id as page_id', 'p.page_type', + 'pc.id as content_id', 'pc.order', + 'n.display_mode as display_mode_default', + 's.id as share_id', 's.receiver', 's.receiver_type' + ) + ->from($this->table, 'c'); + + if ($userId !== null) { + $this->applyOwnedOrSharedQuery($qb, $userId); + $qb->addSelect('n2.display_mode'); + $qb->leftJoin('s', 'tables_contexts_navigation', 'n2', $qb->expr()->andX( + $qb->expr()->eq('s.id', 'n2.share_id'), + $qb->expr()->eq('n2.user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)), + )); + } else { + $qb->leftJoin('c', 'tables_shares', 's', $qb->expr()->andX( + $qb->expr()->eq('c.id', 's.node_id'), + $qb->expr()->eq('s.node_type', $qb->createNamedParameter('context')), + )); + } + + $qb->leftJoin('c', 'tables_contexts_page', 'p', $qb->expr()->eq('c.id', 'p.context_id')); + $qb->leftJoin('p', 'tables_contexts_page_content', 'pc', $qb->expr()->eq('p.id', 'pc.page_id')); + $qb->leftJoin('c', 'tables_contexts_rel_context_node', 'r', $qb->expr()->eq('c.id', 'r.context_id')); + $qb->leftJoin('s', 'tables_contexts_navigation', 'n', $qb->expr()->andX( + $qb->expr()->eq('s.id', 'n.share_id'), + $qb->expr()->eq('n.user_id', $qb->createNamedParameter('')), + )); + + $qb->andWhere($qb->expr()->orX( + $qb->expr()->eq('pc.node_rel_id', 'r.id'), + $qb->expr()->isNull('pc.node_rel_id'), + )); + + $qb->orderBy('pc.order', 'ASC'); + + return $qb; + } + + protected function formatResultRows(array $rows, ?string $userId) { + $formatted = [ + 'id' => $rows[0]['id'], + 'name' => $rows[0]['name'], + 'icon' => $rows[0]['icon'], + 'description' => $rows[0]['description'], + 'owner_id' => $rows[0]['owner_id'], + 'owner_type' => $rows[0]['owner_type'], + ]; + + $formatted['sharing'] = array_reduce($rows, function (array $carry, array $item) use ($userId) { + if ($item['share_id'] === null) { + // empty Context + return $carry; + } + $carry[$item['share_id']] = [ + 'share_id' => $item['share_id'], + 'receiver' => $item['receiver'], + 'receiver_type' => $item['receiver_type'], + 'display_mode_default' => $item['display_mode_default'], + ]; + if ($userId !== null) { + $carry[$item['share_id']]['display_mode'] = $item['display_mode']; + } + return $carry; + }, []); + + $formatted['nodes'] = array_reduce($rows, function (array $carry, array $item) { + if ($item['node_rel_id'] === null) { + // empty Context + return $carry; + } + $carry[$item['node_rel_id']] = [ + 'id' => $item['node_rel_id'], + 'node_id' => $item['node_id'], + 'node_type' => $item['node_type'], + 'permissions' => $item['permissions'], + ]; + return $carry; + }, []); + + $formatted['pages'] = array_reduce($rows, function (array $carry, array $item) { + if ($item['page_id'] === null) { + // empty Context + return $carry; + } + if (!isset($carry[$item['page_id']])) { + $carry[$item['page_id']] = ['content' => []]; + } + $carry[$item['page_id']]['id'] = $item['page_id']; + $carry[$item['page_id']]['page_type'] = $item['page_type']; + if ($item['node_rel_id'] !== null) { + $carry[$item['page_id']]['content'][$item['content_id']] = [ + 'order' => $item['order'], + 'node_rel_id' => $item['node_rel_id'] + ]; + } + + return $carry; + }, []); + + return $this->mapRowToEntity($formatted); + } + + /** + * @return Context[] + * @throws Exception + */ + public function findAll(?string $userId = null): array { + $qb = $this->getFindContextBaseQuery($userId); + + $result = $qb->executeQuery(); + $r = $result->fetchAll(); + + $contextIds = []; + foreach ($r as $row) { + $contextIds[$row['id']] = 1; + } + $contextIds = array_keys($contextIds); + unset($row); + + $resultEntities = []; + foreach ($contextIds as $contextId) { + $workArray = []; + foreach ($r as $row) { + if ($row['id'] === $contextId) { + $workArray[] = $row; + } + } + $resultEntities[] = $this->formatResultRows($workArray, $userId); + } + + return $resultEntities; + } + + /** + * @throws Exception + * @throws NotFoundError + */ + public function findById(int $contextId, ?string $userId = null): Context { + $qb = $this->getFindContextBaseQuery($userId); + $qb->andWhere($qb->expr()->eq('c.id', $qb->createNamedParameter($contextId, IQueryBuilder::PARAM_INT))); + + $result = $qb->executeQuery(); + $r = $result->fetchAll(); + + if (empty($r)) { + throw new NotFoundError('Context does not exist'); + } + + return $this->formatResultRows($r, $userId); + } + + protected function applyOwnedOrSharedQuery(IQueryBuilder $qb, string $userId): void { + $sharedToConditions = $qb->expr()->orX(); + + // shared to user clause + $userShare = $qb->expr()->andX( + $qb->expr()->eq('s.receiver_type', $qb->createNamedParameter('user')), + $qb->expr()->eq('s.receiver', $qb->createNamedParameter($userId)), + ); + $sharedToConditions->add($userShare); + + // shared to group clause + $groupIDs = $this->userHelper->getGroupIdsForUser($userId); + if (!empty($groupIDs)) { + $groupShares = $qb->expr()->andX( + $qb->expr()->eq('s.receiver_type', $qb->createNamedParameter('group')), + $qb->expr()->in('s.receiver', $qb->createNamedParameter($groupIDs, IQueryBuilder::PARAM_STR_ARRAY)), + ); + $sharedToConditions->add($groupShares); + } + + // owned contexts + apply share conditions + $qb->leftJoin('c', 'tables_shares', 's', $qb->expr()->andX( + $qb->expr()->eq('c.id', 's.node_id'), + $qb->expr()->eq('s.node_type', $qb->createNamedParameter('context')), + $sharedToConditions, + )); + + $whereExpression = $qb->expr()->orX( + $qb->expr()->eq('owner_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)), + $qb->expr()->isNotNull('s.receiver'), + ); + if ($qb->getQueryPart('where') === null) { + $qb->where($whereExpression); + } else { + $qb->andWhere($whereExpression); + } + } +} diff --git a/lib/Db/ContextNodeRelation.php b/lib/Db/ContextNodeRelation.php new file mode 100644 index 000000000..7981ffcd1 --- /dev/null +++ b/lib/Db/ContextNodeRelation.php @@ -0,0 +1,39 @@ +addType('id', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'contextId' => $this->getContextId(), + 'nodeId' => $this->getNodeId(), + 'nodeType' => $this->getNodeType(), + 'permissions' => $this->getPermissions() + ]; + } +} diff --git a/lib/Db/ContextNodeRelationMapper.php b/lib/Db/ContextNodeRelationMapper.php new file mode 100644 index 000000000..7709ca5de --- /dev/null +++ b/lib/Db/ContextNodeRelationMapper.php @@ -0,0 +1,35 @@ + */ +class ContextNodeRelationMapper extends QBMapper { + protected string $table = 'tables_contexts_rel_context_node'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, ContextNodeRelation::class); + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + */ + public function findById(int $nodeRelId): ContextNodeRelation { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($nodeRelId))); + + $row = $this->findOneQuery($qb); + return $this->mapRowToEntity($row); + } +} diff --git a/lib/Db/Page.php b/lib/Db/Page.php new file mode 100644 index 000000000..a7757fb1e --- /dev/null +++ b/lib/Db/Page.php @@ -0,0 +1,32 @@ +addType('id', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'contextId' => $this->getContextId(), + 'pageType' => $this->getPageType(), + ]; + } +} diff --git a/lib/Db/PageContent.php b/lib/Db/PageContent.php new file mode 100644 index 000000000..45bb1e735 --- /dev/null +++ b/lib/Db/PageContent.php @@ -0,0 +1,34 @@ +addType('id', 'integer'); + } + + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'pageId' => $this->getPageId(), + 'nodeRelId' => $this->getNodeRelId(), + 'order' => $this->getOrder(), + ]; + } +} diff --git a/lib/Db/PageContentMapper.php b/lib/Db/PageContentMapper.php new file mode 100644 index 000000000..dcd88fc01 --- /dev/null +++ b/lib/Db/PageContentMapper.php @@ -0,0 +1,60 @@ + */ +class PageContentMapper extends QBMapper { + protected string $table = 'tables_contexts_page_content'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, PageContent::class); + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function findById(int $pageContentId): PageContent { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($pageContentId))); + + return $this->mapRowToEntity($this->findOneQuery($qb)); + } + + /** + * @throws Exception + */ + public function findByPageAndNodeRelation(int $pageId, int $nodeRelId): ?PageContent { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table) + ->where($qb->expr()->andX( + $qb->expr()->eq('page_id', $qb->createNamedParameter($pageId)), + $qb->expr()->eq('node_rel_id', $qb->createNamedParameter($nodeRelId)), + )); + + $result = $qb->executeQuery(); + $r = $result->fetch(); + return $r ? $this->mapRowToEntity($r) : null; + } + + public function findByNodeRelation(int $nodeRelId): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->table) + ->where($qb->expr()->andX( + $qb->expr()->eq('node_rel_id', $qb->createNamedParameter($nodeRelId)), + )); + + return $this->findEntities($qb); + } +} diff --git a/lib/Db/PageMapper.php b/lib/Db/PageMapper.php new file mode 100644 index 000000000..6ade694f1 --- /dev/null +++ b/lib/Db/PageMapper.php @@ -0,0 +1,15 @@ + */ +class PageMapper extends QBMapper { + protected string $table = 'tables_contexts_page'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, $this->table, Page::class); + } +} diff --git a/lib/Errors/BadRequestError.php b/lib/Errors/BadRequestError.php new file mode 100644 index 000000000..71fdb8949 --- /dev/null +++ b/lib/Errors/BadRequestError.php @@ -0,0 +1,6 @@ +reflector = $reflector; + $this->permissionsService = $permissionsService; + $this->userId = $userId; + $this->request = $request; + } + + /** + * @throws PermissionError + * @throws InternalError + */ + public function beforeController($controller, $methodName): void { + // we can have type hinting in the signature only after dropping NC26 – calling parent to enforce on newer releases + parent::beforeController($controller, $methodName); + $this->assertCanManageNode(); + $this->assertCanManageContext(); + } + + /** + * @throws PermissionError + * @throws InternalError + */ + protected function assertCanManageNode(): void { + if ($this->reflector->hasAnnotation('CanManageNode')) { + $nodeId = $this->request->getParam('nodeId'); + $nodeType = $this->request->getParam('nodeType'); + + if (!is_numeric($nodeId) || !is_numeric($nodeType)) { + throw new InternalError('Cannot identify node'); + } + + if ($this->userId === null) { + throw new PermissionError('User not authenticated'); + } + + if (!$this->permissionsService->canManageNodeById((int)$nodeType, (int)$nodeId, $this->userId)) { + throw new PermissionError(sprintf('User %s cannot manage node %d (type %d)', + $this->userId, (int)$nodeId, (int)$nodeType + )); + } + } + } + + /** + * @throws PermissionError + * @throws InternalError + */ + protected function assertCanManageContext(): void { + if ($this->reflector->hasAnnotation('CanManageContext')) { + $contextId = $this->request->getParam('contextId'); + + if (!is_numeric($contextId)) { + throw new InternalError('Cannot identify context'); + } + + if ($this->userId === null) { + throw new PermissionError('User not authenticated'); + } + + if (!$this->permissionsService->canManageContextById((int)$contextId, $this->userId)) { + throw new PermissionError(sprintf('User %s cannot manage context %d', + $this->userId, (int)$contextId + )); + } + } + } +} diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 172b760a0..d738a3c0d 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -128,6 +128,15 @@ * errors_parsing_count: int, * errors_count: int, * } + * + * @psalm-type TablesContext = array{ + * id: int, + * name: string, + * iconName: string, + * description: string, + * owner: string, + * ownerType: int, + * } */ class ResponseDefinitions { } diff --git a/lib/Service/ContextService.php b/lib/Service/ContextService.php new file mode 100644 index 000000000..ee31cc7ad --- /dev/null +++ b/lib/Service/ContextService.php @@ -0,0 +1,426 @@ +contextMapper = $contextMapper; + $this->isCLI = $isCLI; + $this->logger = $logger; + $this->contextNodeRelMapper = $contextNodeRelationMapper; + $this->pageMapper = $pageMapper; + $this->pageContentMapper = $pageContentMapper; + $this->permissionsService = $permissionsService; + $this->userManager = $userManager; + $this->eventDispatcher = $eventDispatcher; + } + + /** + * @return Context[] + * @throws Exception + * @throws InternalError + */ + public function findAll(?string $userId): array { + if ($userId !== null && trim($userId) === '') { + $userId = null; + } + if ($userId === null && !$this->isCLI) { + $error = 'Try to set no user in context, but request is not allowed.'; + $this->logger->warning($error); + throw new InternalError($error); + } + return $this->contextMapper->findAll($userId); + } + + /** + * @throws Exception + * @throws InternalError + * @throws NotFoundError + */ + public function findById(int $id, ?string $userId): Context { + if ($userId !== null && trim($userId) === '') { + $userId = null; + } + if ($userId === null && !$this->isCLI) { + $error = 'Try to set no user in context, but request is not allowed.'; + $this->logger->warning($error); + throw new InternalError($error); + } + + return $this->contextMapper->findById($id, $userId); + } + + /** + * @throws Exception + */ + public function create(string $name, string $iconName, string $description, array $nodes, string $ownerId, int $ownerType): Context { + $context = new Context(); + $context->setName(trim($name)); + $context->setIcon(trim($iconName)); + $context->setDescription(trim($description)); + $context->setOwnerId($ownerId); + $context->setOwnerType($ownerType); + + $this->contextMapper->insert($context); + + if (!empty($nodes)) { + $context->resetUpdatedFields(); + $this->insertNodesFromArray($context, $nodes); + $this->insertPage($context); + } + + return $context; + } + + /** + * @throws Exception + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + */ + public function update(int $contextId, string $userId, ?string $name, ?string $iconName, ?string $description, ?array $nodes): Context { + $context = $this->contextMapper->findById($contextId, $userId); + + if ($name !== null) { + $context->setName(trim($name)); + } + if ($iconName !== null) { + $context->setIcon(trim($iconName)); + } + if ($description !== null) { + $context->setDescription(trim($description)); + } + + $hasUpdatedNodeInformation = false; + if ($nodes !== null) { + $currentNodes = $context->getNodes(); + $currentPages = $context->getPages(); + + $nodesBeingRemoved = []; + $nodesBeingAdded = []; + $nodesBeingKept = []; + + // new node relationships do not have an ID. We can recognize them + // through their nodeType and nodeIds. For this we need to transform + // the known relationships` keys to a compatible format. + $oldNodeResolvableIdMapper = []; + foreach ($currentNodes as $i => $oldNode) { + $key = sprintf('t%di%d', $oldNode['node_type'], $oldNode['node_id']); + $oldNodeResolvableIdMapper[$key] = $i; + } + + foreach ($nodes as $node) { + $key = sprintf('t%di%d', $node['type'], $node['id']); + if (isset($oldNodeResolvableIdMapper[$key])) { + unset($oldNodeResolvableIdMapper[$key]); + $nodesBeingKept[$key] = $node; + continue; + } + $nodesBeingAdded[$key] = $node; + } + + foreach (array_diff_key($oldNodeResolvableIdMapper, $nodesBeingAdded, $nodesBeingKept) as $toRemoveId) { + $nodesBeingRemoved[$toRemoveId] = $currentNodes[$toRemoveId]; + } + unset($nodesBeingKept); + + $hasUpdatedNodeInformation = !empty($nodesBeingAdded) || !empty($nodesBeingRemoved); + + foreach ($nodesBeingRemoved as $node) { + /** @var ContextNodeRelation $removedNode */ + /** @var PageContent[] $removedContents */ + [$removedNode, $removedContents] = $this->removeNodeFromContextAndPages($node['id']); + foreach ($removedContents as $removedContent) { + unset($currentPages[$removedContent->getPageId()]['content'][$removedContent->getId()]); + } + unset($currentNodes[$removedNode->getId()]); + } + unset($nodesBeingRemoved); + + foreach ($nodesBeingAdded as $node) { + /** @var ContextNodeRelation $addedNode */ + /** @var PageContent $updatedContent */ + [$addedNode, $updatedContent] = $this->addNodeToContextAndStartpage( + $contextId, + $node['id'], + $node['type'], + $node['permissions'], + $node['order'] ?? 100, + $userId + ); + $currentNodes[$addedNode->getId()] = $addedNode->jsonSerialize(); + $currentPages[$updatedContent->getPageId()]['content'][$updatedContent->getId()] = $updatedContent->jsonSerialize(); + } + unset($nodesBeingAdded); + } + + $context = $this->contextMapper->update($context); + if ($hasUpdatedNodeInformation && isset($currentNodes) && isset($currentPages)) { + $context->setNodes($currentNodes); + $context->setPages($currentPages); + } + return $context; + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + * @throws BadRequestError + */ + public function transfer(int $contextId, string $newOwnerId, int $newOwnerType): Context { + $context = $this->contextMapper->findById($contextId); + + // the owner type check can be dropped as soon as NC 29 is the lowest supported version, + // as the int range as defined in the Controller will be enforced by the Http/Dispatcher. + if ($newOwnerType !== Application::OWNER_TYPE_USER) { + throw new BadRequestError('Unsupported owner type'); + } + + if (!$this->userManager->userExists($newOwnerId)) { + throw new BadRequestError('User does not exist'); + } + + $context->setOwnerId($newOwnerId); + $context->setOwnerType($newOwnerType); + + $context = $this->contextMapper->update($context); + + $auditEvent = new CriticalActionPerformedEvent( + sprintf('Tables application with ID %d was transferred to user %s', + $contextId, $newOwnerId, + ) + ); + + $this->eventDispatcher->dispatchTyped($auditEvent); + + return $context; + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + */ + public function addNodeToContextById(int $contextId, int $nodeId, int $nodeType, int $permissions, ?string $userId): ContextNodeRelation { + $context = $this->contextMapper->findById($contextId, $userId); + return $this->addNodeToContext($context, $nodeId, $nodeType, $permissions); + } + + /** + * @throws Exception + */ + public function removeNodeFromContext(ContextNodeRelation $nodeRelation): ContextNodeRelation { + return $this->contextNodeRelMapper->delete($nodeRelation); + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function removeNodeFromContextById(int $nodeRelationId): ContextNodeRelation { + $nodeRelation = $this->contextNodeRelMapper->findById($nodeRelationId); + return $this->contextNodeRelMapper->delete($nodeRelation); + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + */ + public function addNodeToContextAndStartpage(int $contextId, int $nodeId, int $nodeType, int $permissions, int $order, string $userId): array { + $relation = $this->addNodeToContextById($contextId, $nodeId, $nodeType, $permissions, $userId); + $pageContent = $this->addNodeRelToPage($relation, $order); + return [$relation, $pageContent]; + } + + /** + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws Exception + */ + public function removeNodeFromContextAndPages(int $nodeRelationId): array { + $nodeRelation = $this->removeNodeFromContextById($nodeRelationId); + $contents = $this->removeNodeRelFromAllPages($nodeRelation); + return [$nodeRelation, $contents]; + } + + /** + * @throws Exception + */ + public function addNodeToContext(Context $context, int $nodeId, int $nodeType, int $permissions): ContextNodeRelation { + $contextNodeRel = new ContextNodeRelation(); + $contextNodeRel->setContextId($context->getId()); + $contextNodeRel->setNodeId($nodeId); + $contextNodeRel->setNodeType($nodeType); + $contextNodeRel->setPermissions($permissions); + + return $this->contextNodeRelMapper->insert($contextNodeRel); + } + + public function addNodeRelToPage(ContextNodeRelation $nodeRel, int $order = null, ?int $pageId = null): PageContent { + if ($pageId === null) { + // when no page is given, find the startpage to add it to + $context = $this->contextMapper->findById($nodeRel->getContextId()); + $pages = $context->getPages(); + foreach ($pages as $page) { + if ($page['page_type'] === 'startpage') { + $pageId = $page['id']; + break; + } + } + } + + $pageContent = $this->pageContentMapper->findByPageAndNodeRelation($pageId, $nodeRel->getId()); + + if ($pageContent === null) { + $pageContent = new PageContent(); + $pageContent->setPageId($pageId); + $pageContent->setNodeRelId($nodeRel->getId()); + $pageContent->setOrder($order ?? 100); //FIXME: demand or calc order + + $pageContent = $this->pageContentMapper->insert($pageContent); + } + return $pageContent; + } + + public function removeNodeRelFromAllPages(ContextNodeRelation $nodeRelation): array { + $contents = $this->pageContentMapper->findByNodeRelation($nodeRelation->getId()); + /** @var PageContent $content */ + foreach ($contents as $content) { + try { + $this->pageContentMapper->delete($content); + } catch (Exception $e) { + $this->logger->warning('Failed to delete Contexts page content with ID {pcId}', [ + 'pcId' => $content->getId(), + 'exception' => $e, + ]); + } + } + return $contents; + } + + public function updateContentOrder(int $pageId, array $contents): array { + $updated = []; + foreach ($contents as $content) { + try { + $updated[] = $this->updatePageContent($pageId, $content['id'], $content['order']); + } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception|InvalidArgumentException $e) { + $this->logger->info('Could not updated order of content with ID {cID}', [ + 'cID' => $content['id'], + 'exception' => $e, + ]); + } + } + return $updated; + } + + /** + * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws Exception + * @throws InvalidArgumentException + */ + protected function updatePageContent(int $pageId, int $contentId, int $order): PageContent { + $pageContent = $this->pageContentMapper->findById($contentId); + if ($pageContent->getPageId() !== $pageId) { + throw new InvalidArgumentException('Content does not belong to given page'); + } + $pageContent->setOrder($order); + return $this->pageContentMapper->update($pageContent); + } + + protected function insertPage(Context $context): void { + $page = new Page(); + $page->setContextId($context->getId()); + $page->setPageType(Page::TYPE_STARTPAGE); + $this->pageMapper->insert($page); + + $addedPage = $page->jsonSerialize(); + + $i = 1; + foreach ($context->getNodes() as $node) { + $pageContent = new PageContent(); + $pageContent->setPageId($page->getId()); + $pageContent->setNodeRelId($node['id']); + $pageContent->setOrder(10 * $i++); + + $this->pageContentMapper->insert($pageContent); + + $addedPage['content'][$pageContent->getId()] = $pageContent->jsonSerialize(); + // the content is already embedded in the page + unset($addedPage['content'][$pageContent->getId()]['pageId']); + } + + $context->setPages($addedPage); + } + + protected function insertNodesFromArray(Context $context, array $nodes): void { + $addedNodes = []; + + $userId = $context->getOwnerType() === Application::OWNER_TYPE_USER ? $context->getOwnerId() : null; + foreach ($nodes as $node) { + try { + if (!$this->permissionsService->canManageNodeById($node['type'], $node['id'], $userId)) { + throw new PermissionError(sprintf('Owner cannot manage node %d (type %d)', $node['id'], $node['type'])); + } + $contextNodeRel = $this->addNodeToContext($context, $node['id'], $node['type'], $node['permissions'] ?? 660); + $addedNodes[] = $contextNodeRel->jsonSerialize(); + } catch (Exception $e) { + $this->logger->warning('Could not add node {ntype}/{nid} to context {cid}, skipping.', [ + 'app' => Application::APP_ID, + 'ntype' => $node['type'], + 'nid' => $node['id'], + 'cid' => $context['id'], + 'exception' => $e, + ]); + } + } + $context->setNodes($addedNodes); + } +} diff --git a/lib/Service/PermissionsService.php b/lib/Service/PermissionsService.php index 8911867bd..922bd4685 100644 --- a/lib/Service/PermissionsService.php +++ b/lib/Service/PermissionsService.php @@ -3,6 +3,7 @@ namespace OCA\Tables\Service; use OCA\Tables\AppInfo\Application; +use OCA\Tables\Db\ContextMapper; use OCA\Tables\Db\Share; use OCA\Tables\Db\ShareMapper; use OCA\Tables\Db\Table; @@ -31,8 +32,18 @@ class PermissionsService { protected ?string $userId = null; protected bool $isCli = false; - - public function __construct(LoggerInterface $logger, ?string $userId, TableMapper $tableMapper, ViewMapper $viewMapper, ShareMapper $shareMapper, UserHelper $userHelper, bool $isCLI) { + private ContextMapper $contextMapper; + + public function __construct( + LoggerInterface $logger, + ?string $userId, + TableMapper $tableMapper, + ViewMapper $viewMapper, + ShareMapper $shareMapper, + ContextMapper $contextMapper, + UserHelper $userHelper, + bool $isCLI + ) { $this->tableMapper = $tableMapper; $this->viewMapper = $viewMapper; $this->shareMapper = $shareMapper; @@ -40,6 +51,7 @@ public function __construct(LoggerInterface $logger, ?string $userId, TableMappe $this->logger = $logger; $this->userId = $userId; $this->isCli = $isCLI; + $this->contextMapper = $contextMapper; } @@ -118,6 +130,28 @@ public function canManageNodeById(int $nodeType, int $nodeId, ?string $userId = return false; } + public function canManageContextById(int $contextId, ?string $userId = null): bool { + try { + $context = $this->contextMapper->findById($contextId, $userId); + } catch (DoesNotExistException $e) { + $this->logger->warning('Context does not exist'); + return false; + } catch (MultipleObjectsReturnedException $e) { + $this->logger->warning('Multiple contexts found for this ID'); + return false; + } catch (Exception $e) { + $this->logger->warning($e->getMessage()); + return false; + } + + if ($context->getOwnerType() !== Application::OWNER_TYPE_USER) { + $this->logger->warning('Unsupported owner type'); + return false; + } + + return $context->getOwnerId() === $userId; + } + public function canAccessView(View $view, ?string $userId = null): bool { if($this->basisCheck($view, 'view', $userId)) { return true; diff --git a/openapi.json b/openapi.json index 864136c15..b6af3bbf1 100644 --- a/openapi.json +++ b/openapi.json @@ -175,6 +175,39 @@ } } }, + "Context": { + "type": "object", + "required": [ + "id", + "name", + "iconName", + "description", + "owner", + "ownerType" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "iconName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "ownerType": { + "type": "integer", + "format": "int64" + } + } + }, "ImportState": { "type": "object", "required": [ @@ -7852,6 +7885,1390 @@ } } } + }, + "/ocs/v2.php/apps/tables/api/2/contexts": { + "get": { + "operationId": "context-list", + "summary": "[api v3] Get all contexts available to the requesting person", + "description": "Return an empty array if no contexts were found", + "tags": [ + "context" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "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": "reporting in available contexts", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Context" + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + }, + "post": { + "operationId": "context-create", + "summary": "[api v2] Create a new context and return it", + "tags": [ + "context" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Name of the context", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "iconName", + "in": "query", + "description": "Material design icon name of the context", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "description", + "in": "query", + "description": "Descriptive text of the context", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "nodes", + "in": "query", + "description": "optional nodes to be connected to this context", + "schema": { + "default": [], + "oneOf": [ + { + "type": "object", + "required": [ + "id", + "type", + "permissions" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "integer", + "format": "int64" + }, + "permissions": { + "type": "integer", + "format": "int64" + } + } + }, + { + "type": "array", + "maxLength": 0 + } + ] + } + }, + { + "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": "returning the full context information", + "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/Context" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}": { + "get": { + "operationId": "context-show", + "summary": "[api v2] Get information about the requests context", + "tags": [ + "context" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "contextId", + "in": "path", + "description": "ID of the context", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "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": "returning the full context information", + "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/Context" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "context not found or not available anymore", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + }, + "put": { + "operationId": "context-update", + "summary": "[api v2] Update an existing context and return it", + "tags": [ + "context" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "provide this parameter to set a new name", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "iconName", + "in": "query", + "description": "provide this parameter to set a new icon", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "description", + "in": "query", + "description": "provide this parameter to set a new description", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "nodes", + "in": "query", + "description": "provide this parameter to set a new list of nodes.", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "contextId", + "in": "path", + "description": "ID of the context", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "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": "returning the full context information", + "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/Context" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}/transfer": { + "put": { + "operationId": "context-transfer", + "summary": "[api v2] Transfer the ownership of a context and return it", + "tags": [ + "context" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "newOwnerId", + "in": "query", + "description": "ID of the new owner", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "newOwnerType", + "in": "query", + "description": "any Application::OWNER_TYPE_* constant", + "schema": { + "type": "integer", + "format": "int64", + "default": 0, + "minimum": 0, + "maximum": 0 + } + }, + { + "name": "contextId", + "in": "path", + "description": "ID of the context", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "minimum": 0 + } + }, + { + "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": "Ownership transferred", + "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/Context" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}/nodes": { + "post": { + "operationId": "context-add-node", + "summary": "[api v2] Add a node to a Context", + "tags": [ + "context" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "nodeId", + "in": "query", + "description": "ID of the node", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "nodeType", + "in": "query", + "description": "any Application::NODE_TYPE_* constant", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "permissions", + "in": "query", + "description": "bitmask of the permissions for context recipients", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "order", + "in": "query", + "description": "in which order the node should appear within the context", + "schema": { + "type": "integer", + "format": "int64", + "nullable": true + } + }, + { + "name": "contextId", + "in": "path", + "description": "ID of the context", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "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": "Node added successfully", + "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/Context" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}/nodes/{nodeRelId}": { + "delete": { + "operationId": "context-remove-node", + "summary": "[api v2] Remove a node from a Context", + "tags": [ + "context" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "contextId", + "in": "path", + "description": "ID of the context", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "nodeRelId", + "in": "path", + "description": "ID of the node-in-context relation", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "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": "Node removed successfully", + "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/Context" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}/pages/{pageId}": { + "put": { + "operationId": "context-update-content-order", + "summary": "[api v2] Update the order on a page of a context", + "tags": [ + "context" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "content", + "in": "query", + "description": "content items with it and order values", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "contextId", + "in": "path", + "description": "ID of the context", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "pageId", + "in": "path", + "description": "ID of the page", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "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": "content updated successfully", + "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/Context" + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } } }, "tags": [] diff --git a/psalm.xml b/psalm.xml index 745419a77..651516ea3 100644 --- a/psalm.xml +++ b/psalm.xml @@ -36,6 +36,7 @@ + diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index c7e693067..96936503a 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -191,6 +191,37 @@ export type paths = { /** [api v2] Remove a node (table or view) to from favorites */ delete: operations["api_favorite-destroy"]; }; + "/ocs/v2.php/apps/tables/api/2/contexts": { + /** + * [api v3] Get all contexts available to the requesting person + * @description Return an empty array if no contexts were found + */ + get: operations["context-list"]; + /** [api v2] Create a new context and return it */ + post: operations["context-create"]; + }; + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}": { + /** [api v2] Get information about the requests context */ + get: operations["context-show"]; + /** [api v2] Update an existing context and return it */ + put: operations["context-update"]; + }; + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}/transfer": { + /** [api v2] Transfer the ownership of a context and return it */ + put: operations["context-transfer"]; + }; + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}/nodes": { + /** [api v2] Add a node to a Context */ + post: operations["context-add-node"]; + }; + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}/nodes/{nodeRelId}": { + /** [api v2] Remove a node from a Context */ + delete: operations["context-remove-node"]; + }; + "/ocs/v2.php/apps/tables/api/2/contexts/{contextId}/pages/{pageId}": { + /** [api v2] Update the order on a page of a context */ + put: operations["context-update-content-order"]; + }; }; export type webhooks = Record; @@ -240,6 +271,16 @@ export type components = { selectionDefault: string; datetimeDefault: string; }; + Context: { + /** Format: int64 */ + id: number; + name: string; + iconName: string; + description: string; + owner: string; + /** Format: int64 */ + ownerType: number; + }; ImportState: { /** Format: int64 */ found_columns_count: number; @@ -2923,4 +2964,471 @@ export type operations = { }; }; }; + /** + * [api v3] Get all contexts available to the requesting person + * @description Return an empty array if no contexts were found + */ + "context-list": { + parameters: { + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + }; + responses: { + /** @description reporting in available contexts */ + 200: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Context"][]; + }; + }; + }; + }; + 500: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + }; + }; + /** [api v2] Create a new context and return it */ + "context-create": { + parameters: { + query: { + /** @description Name of the context */ + name: string; + /** @description Material design icon name of the context */ + iconName: string; + /** @description Descriptive text of the context */ + description?: string; + /** @description optional nodes to be connected to this context */ + nodes?: OneOf<[{ + /** Format: int64 */ + id: number; + /** Format: int64 */ + type: number; + /** Format: int64 */ + permissions: number; + }, unknown[]]>; + }; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + }; + responses: { + /** @description returning the full context information */ + 200: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Context"]; + }; + }; + }; + }; + 500: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + }; + }; + /** [api v2] Get information about the requests context */ + "context-show": { + parameters: { + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + /** @description ID of the context */ + contextId: number; + }; + }; + responses: { + /** @description returning the full context information */ + 200: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Context"]; + }; + }; + }; + }; + /** @description context not found or not available anymore */ + 404: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + 500: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + }; + }; + /** [api v2] Update an existing context and return it */ + "context-update": { + parameters: { + query?: { + /** @description provide this parameter to set a new name */ + name?: string | null; + /** @description provide this parameter to set a new icon */ + iconName?: string | null; + /** @description provide this parameter to set a new description */ + description?: string | null; + /** @description provide this parameter to set a new list of nodes. */ + nodes?: string | null; + }; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + /** @description ID of the context */ + contextId: number; + }; + }; + responses: { + /** @description returning the full context information */ + 200: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Context"]; + }; + }; + }; + }; + /** @description Not found */ + 404: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + 500: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + }; + }; + /** [api v2] Transfer the ownership of a context and return it */ + "context-transfer": { + parameters: { + query: { + /** @description ID of the new owner */ + newOwnerId: string; + /** @description any Application::OWNER_TYPE_* constant */ + newOwnerType?: number; + }; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + /** @description ID of the context */ + contextId: number; + }; + }; + responses: { + /** @description Ownership transferred */ + 200: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Context"]; + }; + }; + }; + }; + /** @description Invalid request */ + 400: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + /** @description Not found */ + 404: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + 500: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + }; + }; + /** [api v2] Add a node to a Context */ + "context-add-node": { + parameters: { + query: { + /** @description ID of the node */ + nodeId: number; + /** @description any Application::NODE_TYPE_* constant */ + nodeType: number; + /** @description bitmask of the permissions for context recipients */ + permissions: number; + /** @description in which order the node should appear within the context */ + order?: number | null; + }; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + /** @description ID of the context */ + contextId: number; + }; + }; + responses: { + /** @description Node added successfully */ + 200: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Context"]; + }; + }; + }; + }; + /** @description Not found */ + 404: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + 500: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + }; + }; + /** [api v2] Remove a node from a Context */ + "context-remove-node": { + parameters: { + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + /** @description ID of the context */ + contextId: number; + /** @description ID of the node-in-context relation */ + nodeRelId: number; + }; + }; + responses: { + /** @description Node removed successfully */ + 200: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Context"]; + }; + }; + }; + }; + /** @description Invalid request */ + 400: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + /** @description Not found */ + 404: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + 500: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + }; + }; + /** [api v2] Update the order on a page of a context */ + "context-update-content-order": { + parameters: { + query: { + /** @description content items with it and order values */ + content: string; + }; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + /** @description ID of the context */ + contextId: number; + /** @description ID of the page */ + pageId: number; + }; + }; + responses: { + /** @description content updated successfully */ + 200: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: components["schemas"]["Context"]; + }; + }; + }; + }; + /** @description Invalid request */ + 400: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + /** @description Not found */ + 404: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + 500: { + content: { + "application/json": { + ocs: { + meta: components["schemas"]["OCSMeta"]; + data: { + message: string; + }; + }; + }; + }; + }; + }; + }; }; diff --git a/tests/unit/Service/PermissionsServiceTest.php b/tests/unit/Service/PermissionsServiceTest.php index f4dcd06a6..6ff0e2a89 100644 --- a/tests/unit/Service/PermissionsServiceTest.php +++ b/tests/unit/Service/PermissionsServiceTest.php @@ -26,6 +26,7 @@ namespace OCA\Tables\Service; +use OCA\Tables\Db\ContextMapper; use OCA\Tables\Db\ShareMapper; use OCA\Tables\Db\TableMapper; use OCA\Tables\Db\ViewMapper; @@ -35,51 +36,42 @@ use Test\TestCase; class PermissionsServiceTest extends TestCase { - public function testPreCheckUserIdGivenUser() { + protected function getPermissionServiceWithUserId(mixed $userId, bool $isCli = false): PermissionsService { $logger = $this->createMock(LoggerInterface::class); - $userId = "TestUser"; $tableMapper = $this->createMock(TableMapper::class); $shareMapper = $this->createMock(ShareMapper::class); $viewMapper = $this->createMock(ViewMapper::class); + $contextMapper = $this->createMock(ContextMapper::class); $userHelper = $this->createMock(UserHelper::class); - $permissionsService = new PermissionsService($logger, $userId, $tableMapper, $viewMapper, $shareMapper, $userHelper, false); + + return new PermissionsService($logger, $userId, $tableMapper, $viewMapper, $shareMapper, $contextMapper, $userHelper, $isCli); + } + + public function testPreCheckUserIdGivenUser() { + $userId = 'TestUser'; + $permissionsService = $this->getPermissionServiceWithUserId($userId); self::assertEquals($userId, $permissionsService->preCheckUserId($userId)); } public function testPreCheckUserIdNoUser() { - $logger = $this->createMock(LoggerInterface::class); $userId = null; - $tableMapper = $this->createMock(TableMapper::class); - $viewMapper = $this->createMock(ViewMapper::class); - $shareMapper = $this->createMock(ShareMapper::class); - $userHelper = $this->createMock(UserHelper::class); - $permissionsService = new PermissionsService($logger, $userId, $tableMapper, $viewMapper, $shareMapper, $userHelper, false); + $permissionsService = $this->getPermissionServiceWithUserId($userId); self::expectException(InternalError::class); $permissionsService->preCheckUserId($userId); } public function testPreCheckUserIdNoUserButContext() { - $logger = $this->createMock(LoggerInterface::class); $userId = 'john'; - $tableMapper = $this->createMock(TableMapper::class); - $viewMapper = $this->createMock(ViewMapper::class); - $shareMapper = $this->createMock(ShareMapper::class); - $userHelper = $this->createMock(UserHelper::class); - $permissionsService = new PermissionsService($logger, $userId, $tableMapper, $viewMapper, $shareMapper, $userHelper, false); + $permissionsService = $this->getPermissionServiceWithUserId($userId); self::assertEquals($userId, $permissionsService->preCheckUserId(null)); } public function testPreCheckUserIdNoUserNotAllowed() { - $logger = $this->createMock(LoggerInterface::class); $userId = ''; - $tableMapper = $this->createMock(TableMapper::class); - $viewMapper = $this->createMock(ViewMapper::class); - $shareMapper = $this->createMock(ShareMapper::class); - $userHelper = $this->createMock(UserHelper::class); - $permissionsService = new PermissionsService($logger, $userId, $tableMapper, $viewMapper, $shareMapper, $userHelper, false); + $permissionsService = $this->getPermissionServiceWithUserId($userId); self::expectException(InternalError::class); $permissionsService->preCheckUserId($userId, false); @@ -89,13 +81,8 @@ public function testPreCheckUserIdNoUserNotAllowed() { } public function testPreCheckUserIdNoUserAllowed() { - $logger = $this->createMock(LoggerInterface::class); $userId = ''; - $tableMapper = $this->createMock(TableMapper::class); - $viewMapper = $this->createMock(ViewMapper::class); - $shareMapper = $this->createMock(ShareMapper::class); - $userHelper = $this->createMock(UserHelper::class); - $permissionsService = new PermissionsService($logger, $userId, $tableMapper, $viewMapper, $shareMapper, $userHelper, true); + $permissionsService = $this->getPermissionServiceWithUserId($userId, true); self::assertEquals($userId, $permissionsService->preCheckUserId($userId)); }