diff --git a/lib/Db/ContextMapper.php b/lib/Db/ContextMapper.php index 964e3d515..15959e767 100644 --- a/lib/Db/ContextMapper.php +++ b/lib/Db/ContextMapper.php @@ -6,6 +6,7 @@ use OCA\Tables\AppInfo\Application; use OCA\Tables\Errors\NotFoundError; +use OCA\Tables\Helper\GroupHelper; use OCA\Tables\Helper\UserHelper; use OCP\AppFramework\Db\QBMapper; use OCP\DB\Exception; @@ -15,10 +16,8 @@ /** @template-extends QBMapper */ class ContextMapper extends QBMapper { protected string $table = 'tables_contexts_context'; - private UserHelper $userHelper; - public function __construct(IDBConnection $db, UserHelper $userHelper) { - $this->userHelper = $userHelper; + public function __construct(IDBConnection $db, protected UserHelper $userHelper, protected GroupHelper $groupHelper) { parent::__construct($db, $this->table, Context::class); } @@ -86,6 +85,11 @@ protected function formatResultRows(array $rows, ?string $userId) { 'share_id' => (int)$item['share_id'], 'receiver' => $item['receiver'], 'receiver_type' => $item['receiver_type'], + 'receiver_display_name' => match ($item['receiver_type']) { + 'user' => $this->userHelper->getUserDisplayName($item['receiver']), + 'group' => $this->groupHelper->getGroupDisplayName($item['receiver']), + default => $item['receiver'], + }, 'display_mode_default' => (int)$item['display_mode_default'], ]; if ($userId !== null) { diff --git a/lib/Service/ContextService.php b/lib/Service/ContextService.php index a0e247bd8..57224f92c 100644 --- a/lib/Service/ContextService.php +++ b/lib/Service/ContextService.php @@ -160,8 +160,15 @@ public function update(int $contextId, string $userId, ?string $name, ?string $i foreach ($nodes as $node) { $key = sprintf('t%di%d', $node['type'], $node['id']); if (isset($oldNodeResolvableIdMapper[$key])) { - unset($oldNodeResolvableIdMapper[$key]); $nodesBeingKept[$key] = $node; + if ($node['permissions'] !== $currentNodes[$oldNodeResolvableIdMapper[$key]]['permissions']) { + $nodeRel = $this->contextNodeRelMapper->findById($currentNodes[$oldNodeResolvableIdMapper[$key]]['id']); + $nodeRel->setPermissions($node['permissions']); + $this->contextNodeRelMapper->update($nodeRel); + $currentNodes[$oldNodeResolvableIdMapper[$key]]['permissions'] = $nodeRel->getPermissions(); + $hasUpdatedNodeInformation = true; + } + unset($oldNodeResolvableIdMapper[$key]); continue; } $nodesBeingAdded[$key] = $node; @@ -172,7 +179,7 @@ public function update(int $contextId, string $userId, ?string $name, ?string $i } unset($nodesBeingKept); - $hasUpdatedNodeInformation = !empty($nodesBeingAdded) || !empty($nodesBeingRemoved); + $hasUpdatedNodeInformation = $hasUpdatedNodeInformation || !empty($nodesBeingAdded) || !empty($nodesBeingRemoved); foreach ($nodesBeingRemoved as $node) { /** @var ContextNodeRelation $removedNode */ @@ -203,10 +210,11 @@ public function update(int $contextId, string $userId, ?string $name, ?string $i } $context = $this->contextMapper->update($context); - if ($hasUpdatedNodeInformation && isset($currentNodes) && isset($currentPages)) { + if (isset($currentNodes, $currentPages) && $hasUpdatedNodeInformation) { $context->setNodes($currentNodes); $context->setPages($currentPages); } + return $context; } @@ -422,7 +430,7 @@ protected function insertNodesFromArray(Context $context, array $nodes): void { 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); + $contextNodeRel = $this->addNodeToContext($context, $node['id'], $node['type'], $node['permissions'] ?? 0); $addedNodes[] = $contextNodeRel->jsonSerialize(); } catch (Exception $e) { $this->logger->warning('Could not add node {ntype}/{nid} to context {cid}, skipping.', [ diff --git a/lib/Service/PermissionsService.php b/lib/Service/PermissionsService.php index 5bef54b46..93ed1bb8a 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\Context; use OCA\Tables\Db\ContextMapper; use OCA\Tables\Db\Share; use OCA\Tables\Db\ShareMapper; @@ -149,7 +150,7 @@ public function canManageContextById(int $contextId, ?string $userId = null): bo return false; } - return $context->getOwnerId() === $userId; + return $context->getOwnerId() === $userId || $this->canManageContext($context, $userId); } /** @@ -181,6 +182,8 @@ public function canManageElementById(int $elementId, string $nodeType = 'table', return $this->canManageTableById($elementId, $userId); } elseif ($nodeType === 'view') { return $this->canManageViewById($elementId, $userId); + } elseif ($nodeType === 'context') { + return $this->canManageContextById($elementId, $userId); } else { throw new InternalError('Cannot read permission for node type '.$nodeType); } @@ -199,6 +202,10 @@ public function canManageTable(Table $table, ?string $userId = null): bool { return $this->checkPermission($table, 'table', 'manage', $userId); } + public function canManageContext(Context $context, ?string $userId = null): bool { + return $this->checkPermission($context, 'context', 'manage', $userId); + } + public function canManageTableById(int $tableId, ?string $userId = null): bool { try { $table = $this->tableMapper->find($tableId); @@ -516,13 +523,13 @@ private function hasPermission(int $existingPermissions, string $permissionName) } /** - * @param mixed $element - * @param 'table'|'view' $nodeType + * @param Table|View|Context $element + * @param 'table'|'view'|'context' $nodeType * @param string $permission * @param string|null $userId * @return bool */ - private function checkPermission($element, string $nodeType, string $permission, ?string $userId = null): bool { + private function checkPermission(Table|View|Context $element, string $nodeType, string $permission, ?string $userId = null): bool { if($this->basisCheck($element, $nodeType, $userId)) { return true; } @@ -535,7 +542,9 @@ private function checkPermission($element, string $nodeType, string $permission, return $this->getSharedPermissionsIfSharedWithMe($element->getId(), $nodeType, $userId)[$permission]; } catch (NotFoundError $e) { try { - if ($this->hasPermission($this->getPermissionIfAvailableThroughContext($element->getId(), $nodeType, $userId), $permission)) { + if ($nodeType !== 'context' + && $this->hasPermission($this->getPermissionIfAvailableThroughContext($element->getId(), $nodeType, $userId), $permission) + ) { return true; } } catch (NotFoundError $e) { @@ -573,13 +582,7 @@ private function checkPermissionById(int $elementId, string $nodeType, string $p return false; } - /** - * @param Table|View $element - * @param string $nodeType - * @param string|null $userId - * @return bool - */ - private function basisCheck($element, string $nodeType, ?string &$userId): bool { + private function basisCheck(Table|View|Context $element, string $nodeType, ?string &$userId): bool { try { $userId = $this->preCheckUserId($userId); } catch (InternalError $e) { @@ -592,7 +595,7 @@ private function basisCheck($element, string $nodeType, ?string &$userId): bool return true; } - if ($this->userIsElementOwner($element, $userId)) { + if ($this->userIsElementOwner($element, $userId, $nodeType)) { return true; } try { @@ -633,11 +636,14 @@ private function basisCheckById(int $elementId, string $nodeType, ?string &$user } /** - * @param View|Table $element + * @param View|Table|Context $element * @param string|null $userId * @return bool */ - private function userIsElementOwner($element, string $userId = null): bool { + private function userIsElementOwner($element, string $userId = null, ?string $nodeType = null): bool { + if ($nodeType === 'context') { + return $element->getOwnerId() === $userId; + } return $element->getOwnership() === $userId; } diff --git a/lib/Service/TableService.php b/lib/Service/TableService.php index eda2eaa19..2c3bb2884 100644 --- a/lib/Service/TableService.php +++ b/lib/Service/TableService.php @@ -44,6 +44,7 @@ class TableService extends SuperService { protected IL10N $l; + private ContextService $contextService; protected IEventDispatcher $eventDispatcher; @@ -59,8 +60,9 @@ public function __construct( ShareService $shareService, UserHelper $userHelper, FavoritesService $favoritesService, + IEventDispatcher $eventDispatcher, + ContextService $contextService, IL10N $l, - IEventDispatcher $eventDispatcher ) { parent::__construct($logger, $userId, $permissionsService); $this->mapper = $mapper; @@ -73,6 +75,7 @@ public function __construct( $this->favoritesService = $favoritesService; $this->l = $l; $this->eventDispatcher = $eventDispatcher; + $this->contextService = $contextService; } /** @@ -91,9 +94,13 @@ public function __construct( public function findAll(?string $userId = null, bool $skipTableEnhancement = false, bool $skipSharedTables = false, bool $createTutorial = true): array { /** @var string $userId */ $userId = $this->permissionsService->preCheckUserId($userId); // $userId can be set or '' + $allTables = []; try { - $allTables = $this->mapper->findAll($userId); // get own tables + $ownedTables = $this->mapper->findAll($userId); // get own tables + foreach ($ownedTables as $ownedTable) { + $allTables[$ownedTable->getId()] = $ownedTable; + } } catch (OcpDbException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError($e->getMessage()); @@ -102,7 +109,8 @@ public function findAll(?string $userId = null, bool $skipTableEnhancement = fal // if there are no own tables found, create the tutorial table if (count($allTables) === 0 && $createTutorial) { try { - $allTables = [$this->create($this->l->t('Tutorial'), 'tutorial', '🚀')]; + $tutorialTable = $this->create($this->l->t('Tutorial'), 'tutorial', '🚀'); + $allTables[$tutorialTable->getId()] = $tutorialTable; } catch (InternalError|PermissionError|DoesNotExistException|MultipleObjectsReturnedException|OcpDbException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': '.$e->getMessage()); @@ -114,16 +122,22 @@ public function findAll(?string $userId = null, bool $skipTableEnhancement = fal // clean duplicates foreach ($sharedTables as $sharedTable) { - $found = false; - foreach ($allTables as $table) { - if ($sharedTable->getId() === $table->getId()) { - $found = true; - break; - } + if (!isset($allTables[$sharedTable->getId()])) { + $allTables[$sharedTable->getId()] = $sharedTable; } - if (!$found) { - $allTables[] = $sharedTable; + } + } + + $contexts = $this->contextService->findAll($userId); + foreach ($contexts as $context) { + $nodes = $context->getNodes(); + foreach ($nodes as $node) { + if ($node['node_type'] !== Application::NODE_TYPE_TABLE + || isset($allTables[$node['node_id']]) + ) { + continue; } + $allTables[$node['node_id']] = $this->find($node['node_id'], $skipTableEnhancement, $userId); } } @@ -144,8 +158,7 @@ public function findAll(?string $userId = null, bool $skipTableEnhancement = fal } } - - return $allTables; + return array_values($allTables); } /** diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index 83782fd49..bc10a88bb 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -40,6 +40,7 @@ class ViewService extends SuperService { protected FavoritesService $favoritesService; protected IL10N $l; + private ContextService $contextService; protected IEventDispatcher $eventDispatcher; @@ -52,8 +53,9 @@ public function __construct( RowService $rowService, UserHelper $userHelper, FavoritesService $favoritesService, - IL10N $l, - IEventDispatcher $eventDispatcher + IEventDispatcher $eventDispatcher, + ContextService $contextService, + IL10N $l ) { parent::__construct($logger, $userId, $permissionsService); $this->l = $l; @@ -63,6 +65,7 @@ public function __construct( $this->userHelper = $userHelper; $this->favoritesService = $favoritesService; $this->eventDispatcher = $eventDispatcher; + $this->contextService = $contextService; } @@ -148,11 +151,31 @@ public function findSharedViewsWithMe(?string $userId = null): array { if ($userId === '') { return []; } + + $allViews = []; + $sharedViews = $this->shareService->findViewsSharedWithMe($userId); - foreach ($sharedViews as $view) { + foreach ($sharedViews as $sharedView) { + $allViews[$sharedView->getId()] = $sharedView; + } + + $contexts = $this->contextService->findAll($userId); + foreach ($contexts as $context) { + $nodes = $context->getNodes(); + foreach ($nodes as $node) { + if ($node['node_type'] !== Application::NODE_TYPE_VIEW + || isset($allViews[$node['node_id']]) + ) { + continue; + } + $allViews[$node['node_id']] = $this->find($node['node_id'], false, $userId); + } + } + + foreach ($allViews as $view) { $this->enhanceView($view, $userId); } - return $sharedViews; + return array_values($allViews); } diff --git a/src/modules/modals/CreateContext.vue b/src/modules/modals/CreateContext.vue index fa5960675..29109e0f7 100644 --- a/src/modules/modals/CreateContext.vue +++ b/src/modules/modals/CreateContext.vue @@ -37,9 +37,9 @@
{{ t('tables', 'Resources') }}
- + -
+
{{ t('tables', 'Create application') }} @@ -57,6 +57,7 @@ import '@nextcloud/dialogs/dist/index.css' import NcContextResource from '../../shared/components/ncContextResource/NcContextResource.vue' import NcIconPicker from '../../shared/components/ncIconPicker/NcIconPicker.vue' import svgHelper from '../../shared/components/ncIconPicker/mixins/svgHelper.js' +import permissionBitmask from '../../shared/components/ncContextResource/mixins/permissionBitmask.js' export default { name: 'CreateContext', @@ -67,7 +68,7 @@ export default { NcIconSvgWrapper, NcContextResource, }, - mixins: [svgHelper], + mixins: [svgHelper, permissionBitmask], props: { showModal: { type: Boolean, @@ -86,6 +87,7 @@ export default { errorTitle: false, description: '', resources: [], + receivers: [], } }, watch: { @@ -130,7 +132,7 @@ export default { return { id: parseInt(resource.id), type: parseInt(resource.nodeType), - permissions: 660, + permissions: this.getPermissionBitmaskFromBools(true /* ensure read permission is always true */, resource.permissionCreate, resource.permissionUpdate, resource.permissionDelete), } }) const data = { @@ -139,7 +141,7 @@ export default { description: this.description, nodes: dataResources, } - const res = await this.$store.dispatch('insertNewContext', { data }) + const res = await this.$store.dispatch('insertNewContext', { data, previousReceivers: [], receivers: this.receivers }) if (res) { return res.id } else { diff --git a/src/modules/modals/EditContext.vue b/src/modules/modals/EditContext.vue index b6df8bc71..a27036907 100644 --- a/src/modules/modals/EditContext.vue +++ b/src/modules/modals/EditContext.vue @@ -12,11 +12,8 @@
- + @@ -36,10 +33,10 @@
{{ t('tables', 'Resources') }}
- +
-
+
{{ t('tables', 'Save') }} @@ -53,12 +50,14 @@ + + diff --git a/src/shared/components/ncContextResource/ResourceSharees.vue b/src/shared/components/ncContextResource/ResourceSharees.vue new file mode 100644 index 000000000..254a67a17 --- /dev/null +++ b/src/shared/components/ncContextResource/ResourceSharees.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/src/shared/components/ncContextResource/mixins/permissionBitmask.js b/src/shared/components/ncContextResource/mixins/permissionBitmask.js new file mode 100644 index 000000000..26912b581 --- /dev/null +++ b/src/shared/components/ncContextResource/mixins/permissionBitmask.js @@ -0,0 +1,21 @@ +import { PERMISSION_READ, PERMISSION_CREATE, PERMISSION_UPDATE, PERMISSION_DELETE } from '../../../constants.js' + +export default { + data() { + return { + PERMISSION_READ, + PERMISSION_CREATE, + PERMISSION_UPDATE, + PERMISSION_DELETE, + } + }, + methods: { + getPermissionBitmaskFromBools(permissionRead, permissionCreate, permissionUpdate, permissionDelete) { + const read = permissionRead ? PERMISSION_READ : 0 + const create = permissionCreate ? PERMISSION_CREATE : 0 + const update = permissionUpdate ? PERMISSION_UPDATE : 0 + const del = permissionDelete ? PERMISSION_DELETE : 0 + return read | create | update | del + }, + }, +} diff --git a/src/shared/constants.js b/src/shared/constants.js index 5092fae81..ceb32e169 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -21,3 +21,11 @@ export const NODE_TYPE_TABLE = 0 export const NODE_TYPE_VIEW = 1 + +// from Application.php +export const PERMISSION_READ = 1 +export const PERMISSION_CREATE = 2 +export const PERMISSION_UPDATE = 4 +export const PERMISSION_DELETE = 8 +export const PERMISSION_MANAGE = 16 +export const PERMISSION_ALL = 31 diff --git a/src/store/store.js b/src/store/store.js index 02c6a72d5..1ab5e0e4d 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -121,23 +121,6 @@ export default new Vuex.Store({ }, }, actions: { - async insertNewContext({ commit, state, dispatch }, { data }) { - commit('setContextsLoading', true) - let res = null - - try { - res = await axios.post(generateOcsUrl('/apps/tables/api/2/contexts'), data) - } catch (e) { - displayError(e, t('tables', 'Could not insert application.')) - return false - } - const contexts = state.contexts - contexts.push(res.data.ocs.data) - commit('setContexts', contexts) - - commit('setContextsLoading', false) - return res.data.ocs.data - }, async insertNewTable({ commit, state, dispatch }, { data }) { let res = null @@ -339,11 +322,67 @@ export default new Vuex.Store({ return true }, - async updateContext({ state, commit, dispatch }, { id, data }) { + async shareContext({ dispatch }, { id, previousReceivers, receivers }) { + const share = { + nodeType: 'context', + nodeId: id, + displayMode: 2, + } + try { + for (const receiver of receivers) { + share.receiverType = receiver.isUser ? 'user' : 'group' + share.receiver = receiver.user + // Avoid duplicate shares by checking if share exists first + const existingShare = previousReceivers.find((p) => p.receiver === share.receiver && p.receiver_type === share.receiverType) + if (!existingShare) { + await axios.post(generateUrl('/apps/tables/share'), share) + } + } + } catch (e) { + displayError(e, t('tables', 'Could not add application share.')) + } + try { + // If there's a previous share that wasn't maintained, delete it + for (const previousReceiver of previousReceivers) { + const currentShare = receivers.find((r) => { + const receiverType = r.isUser ? 'user' : 'group' + return r.user === previousReceiver.receiver && receiverType === previousReceiver.receiver_type + }) + if (!currentShare) { + await axios.delete(generateUrl('/apps/tables/share/' + previousReceiver.share_id)) + } + } + } catch (e) { + displayError(e, t('tables', 'Could not remove application share.')) + } + }, + async insertNewContext({ commit, state, dispatch }, { data, receivers }) { + commit('setContextsLoading', true) let res = null + try { + res = await axios.post(generateOcsUrl('/apps/tables/api/2/contexts'), data) + const id = res?.data?.ocs?.data?.id + if (id) { + await dispatch('shareContext', { id, previousReceivers: [], receivers }) + } + } catch (e) { + displayError(e, t('tables', 'Could not insert application.')) + return false + } + const contexts = state.contexts + contexts.push(res.data.ocs.data) + commit('setContexts', contexts) + + commit('setContextsLoading', false) + return res.data.ocs.data + }, + async updateContext({ state, commit, dispatch }, { id, data, previousReceivers, receivers }) { + let res = null try { res = await axios.put(generateOcsUrl('/apps/tables/api/2/contexts/' + id), data) + await dispatch('shareContext', { id, previousReceivers, receivers }) + } catch (e) { displayError(e, t('tables', 'Could not update application.')) return false diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 7a4757e24..f6d3005fd 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -611,7 +611,8 @@ public function userTables(string $user, TableNode $body = null): void { $titles[] = $d['title']; } foreach ($body->getRows()[0] as $tableTitle) { - Assert::assertTrue(in_array($tableTitle, $titles, true)); + $message = sprintf('"%s" not in the list: %s', $tableTitle, implode(', ', $titles)); + Assert::assertTrue(in_array($tableTitle, $titles, true), $message); } }