Skip to content

Commit

Permalink
Merge pull request #962 from nextcloud/feat/noid/context-sharing
Browse files Browse the repository at this point in the history
feat(Context): add share logic for contexts
  • Loading branch information
blizzz authored Apr 24, 2024
2 parents 6d20318 + 9598e3f commit 97578bc
Show file tree
Hide file tree
Showing 25 changed files with 1,064 additions and 101 deletions.
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
['name' => 'api1#createShare', 'url' => '/api/1/shares', 'verb' => 'POST'],
['name' => 'api1#deleteShare', 'url' => '/api/1/shares/{shareId}', 'verb' => 'DELETE'],
['name' => 'api1#updateSharePermissions', 'url' => '/api/1/shares/{shareId}', 'verb' => 'PUT'],
['name' => 'api1#updateShareDisplayMode', 'url' => '/api/1/shares/{shareId}/display-mode', 'verb' => 'PUT'],
['name' => 'api1#createTableShare', 'url' => '/api/1/tables/{tableId}/shares', 'verb' => 'POST'],
// -> columns
['name' => 'api1#indexTableColumns', 'url' => '/api/1/tables/{tableId}/columns', 'verb' => 'GET'],
Expand Down Expand Up @@ -97,6 +98,7 @@
['name' => 'share#show', 'url' => '/share/{id}', 'verb' => 'GET'],
['name' => 'share#create', 'url' => '/share', 'verb' => 'POST'],
['name' => 'share#updatePermission', 'url' => '/share/{id}/permission', 'verb' => 'PUT'],
['name' => 'share#updateDisplayMode', 'url' => '/share/{id}/display-mode', 'verb' => 'PUT'],
['name' => 'share#destroy', 'url' => '/share/{id}', 'verb' => 'DELETE'],

// import
Expand Down
68 changes: 65 additions & 3 deletions lib/Controller/Api1Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
* @psalm-import-type TablesColumn from ResponseDefinitions
* @psalm-import-type TablesRow from ResponseDefinitions
* @psalm-import-type TablesImportState from ResponseDefinitions
* @psalm-import-type TablesContextNavigation from ResponseDefinitions
*/
class Api1Controller extends ApiController {
private TableService $tableService;
Expand Down Expand Up @@ -483,15 +484,27 @@ public function indexTableShares(int $tableId): DataResponse {
* @param bool $permissionUpdate Permission if receiver can update data
* @param bool $permissionDelete Permission if receiver can delete data
* @param bool $permissionManage Permission if receiver can manage node
* @param int $displayMode context shares only, whether it should appear in nav bar. 0: no, 1: recipients, 2: all
* @return DataResponse<Http::STATUS_OK, TablesShare, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: Share returned
* 403: No permissions
* 404: Not found
*/
public function createShare(int $nodeId, string $nodeType, string $receiver, string $receiverType, bool $permissionRead = false, bool $permissionCreate = false, bool $permissionUpdate = false, bool $permissionDelete = false, bool $permissionManage = false): DataResponse {
public function createShare(
int $nodeId,
string $nodeType,
string $receiver,
string $receiverType,
bool $permissionRead = false,
bool $permissionCreate = false,
bool $permissionUpdate = false,
bool $permissionDelete = false,
bool $permissionManage = false,
int $displayMode = 0,
): DataResponse {
try {
return new DataResponse($this->shareService->create($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage)->jsonSerialize());
return new DataResponse($this->shareService->create($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage, $displayMode)->jsonSerialize());
} catch (PermissionError $e) {
$this->logger->warning('A permission error occurred: '.$e->getMessage());
$message = ['message' => $e->getMessage()];
Expand Down Expand Up @@ -573,6 +586,55 @@ public function updateSharePermissions(int $shareId, string $permissionType, boo
}
}

/**
* Updates the display mode of a context share
*
* @NoAdminRequired
* @CORS
* @NoCSRFRequired
*
* @param int $shareId Share ID
* @param int $displayMode The new value for the display mode of the nav bar icon. 0: hidden, 1: visible for recipients, 2: visible for all
* @param string $target "default" to set the default, "self" to set an override for the authenticated user
* @return DataResponse<Http::STATUS_OK, TablesContextNavigation, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
*
* 200: Display mode updated
* 400: Invalid parameter
* 403: No permissions
* 404: Share not found
*
* @psalm-param int<0, 2> $displayMode
* @psalm-param ("default"|"self") $target
*/
public function updateShareDisplayMode(int $shareId, int $displayMode, string $target = 'default'): DataResponse {
if ($target === 'default') {
$userId = '';
} elseif ($target === 'self') {
$userId = $this->userId;
} else {
$error = 'target parameter must be either "default" or "self"';
$this->logger->warning(sprintf('An internal error or exception occurred: %s', $error));
$message = ['message' => $error];
return new DataResponse($message, Http::STATUS_BAD_REQUEST);
}

try {
return new DataResponse($this->shareService->updateDisplayMode($shareId, $displayMode, $userId)->jsonSerialize());
} catch (InternalError $e) {
$this->logger->warning('An internal error or exception occurred: '.$e->getMessage());
$message = ['message' => $e->getMessage()];
return new DataResponse($message, Http::STATUS_INTERNAL_SERVER_ERROR);
} catch (NotFoundError $e) {
$this->logger->warning('A not found error occurred: ' . $e->getMessage());
$message = ['message' => $e->getMessage()];
return new DataResponse($message, Http::STATUS_NOT_FOUND);
} catch (PermissionError $e) {
$this->logger->warning('A permission error occurred: '.$e->getMessage());
$message = ['message' => $e->getMessage()];
return new DataResponse($message, Http::STATUS_FORBIDDEN);
}
}

// Columns

/**
Expand Down Expand Up @@ -1303,7 +1365,7 @@ public function importInView(int $viewId, string $path, bool $createMissingColum
*/
public function createTableShare(int $tableId, string $receiver, string $receiverType, bool $permissionRead, bool $permissionCreate, bool $permissionUpdate, bool $permissionDelete, bool $permissionManage): DataResponse {
try {
return new DataResponse($this->shareService->create($tableId, 'table', $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage)->jsonSerialize());
return new DataResponse($this->shareService->create($tableId, 'table', $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage, 0)->jsonSerialize());
} catch (PermissionError $e) {
$this->logger->warning('A permission error occurred: '.$e->getMessage());
$message = ['message' => $e->getMessage()];
Expand Down
36 changes: 33 additions & 3 deletions lib/Controller/ShareController.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,20 @@ public function show(int $id): DataResponse {
/**
* @NoAdminRequired
*/
public function create(int $nodeId, string $nodeType, string $receiver, string $receiverType, bool $permissionRead = false, bool $permissionCreate = false, bool $permissionUpdate = false, bool $permissionDelete = false, bool $permissionManage = false): DataResponse {
return $this->handleError(function () use ($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage) {
return $this->service->create($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage);
public function create(
int $nodeId,
string $nodeType,
string $receiver,
string $receiverType,
bool $permissionRead = false,
bool $permissionCreate = false,
bool $permissionUpdate = false,
bool $permissionDelete = false,
bool $permissionManage = false,
int $displayMode = 0,
): DataResponse {
return $this->handleError(function () use ($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage, $displayMode) {
return $this->service->create($nodeId, $nodeType, $receiver, $receiverType, $permissionRead, $permissionCreate, $permissionUpdate, $permissionDelete, $permissionManage, $displayMode);
});
}

Expand All @@ -76,6 +87,25 @@ public function updatePermission(int $id, string $permission, bool $value): Data
});
}

/**
* @NoAdminRequired
* @psalm-param int<0, 2> $displayMode
* @psalm-param ("default"|"self") $target
*/
public function updateDisplayMode(int $id, int $displayMode, string $target = 'default') {
return $this->handleError(function () use ($id, $displayMode, $target) {
if ($target === 'default') {
$userId = '';
} elseif ($target === 'self') {
$userId = $this->userId;
} else {
throw new \InvalidArgumentException('target parameter must be either "default" or "self"');
}

return $this->service->updateDisplayMode($id, $displayMode, $userId);
});
}

/**
* @NoAdminRequired
*/
Expand Down
14 changes: 11 additions & 3 deletions lib/Db/ContextMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,10 +16,8 @@
/** @template-extends QBMapper<Context> */
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);
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -253,6 +257,10 @@ public function findAllContainingNode(int $nodeType, int $nodeId, string $userId
protected function applyOwnedOrSharedQuery(IQueryBuilder $qb, string $userId): void {
$sharedToConditions = $qb->expr()->orX();

// shared by user clause
$userInitiatedShare = $qb->expr()->eq('s.sender', $qb->createNamedParameter($userId));
$sharedToConditions->add($userInitiatedShare);

// shared to user clause
$userShare = $qb->expr()->andX(
$qb->expr()->eq('s.receiver_type', $qb->createNamedParameter('user')),
Expand Down
35 changes: 35 additions & 0 deletions lib/Db/ContextNavigation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace OCA\Tables\Db;

use OCP\AppFramework\Db\Entity;

/**
* @method getShareId(): int
* @method setShareId(int $value): void
* @method getUserId(): string
* @method setUserId(string $value): void
* @method getDisplayMode(): int
* @method setDisplayMode(int $value): void
*/
class ContextNavigation extends Entity implements \JsonSerializable {
protected ?int $shareId = null;
protected ?string $userId = null;
protected ?int $displayMode = null;

public function __construct() {
$this->addType('shareId', 'integer');
$this->addType('displayMode', 'integer');
}

public function jsonSerialize(): array {
return [
'id' => $this->getId(),
'shareId' => $this->getShareId(),
'displayMode' => $this->getDisplayMode(),
'userId' => $this->getUserId(),
];
}
}
41 changes: 41 additions & 0 deletions lib/Db/ContextNavigationMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace OCA\Tables\Db;

use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

/** @template-extends QBMapper<ContextNavigation> */
class ContextNavigationMapper extends QBMapper {
protected string $table = 'tables_contexts_navigation';

public function __construct(IDBConnection $db) {
parent::__construct($db, $this->table, ContextNavigation::class);
}

/**
* @throws Exception
*/
public function deleteByShareId(int $shareId): int {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->table)
->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId, IQueryBuilder::PARAM_INT)));
return $qb->executeStatement();
}

/**
* @throws Exception
*/
public function setDisplayModeByShareId(int $shareId, int $displayMode, string $userId): ContextNavigation {
$entity = new ContextNavigation();
$entity->setShareId($shareId);
$entity->setDisplayMode($displayMode);
$entity->setUserId($userId);

return $this->insertOrUpdate($entity);
}
}
7 changes: 7 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@
* owner: string,
* ownerType: int,
* }
*
* @psalm-type TablesContextNavigation = array{
* id: int,
* shareId: int,
* displayMode: int,
* userId: string,
* }
*/
class ResponseDefinitions {
}
16 changes: 12 additions & 4 deletions lib/Service/ContextService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 */
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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.', [
Expand Down
Loading

0 comments on commit 97578bc

Please sign in to comment.