Skip to content

Commit

Permalink
feat(Contexts): API to get full context info
Browse files Browse the repository at this point in the history
Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
  • Loading branch information
blizzz committed Feb 26, 2024
1 parent 51a08b6 commit a9086d2
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 26 deletions.
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Have a good time and manage whatever you want.
<command>OCA\Tables\Command\RenameTable</command>
<command>OCA\Tables\Command\ChangeOwnershipTable</command>
<command>OCA\Tables\Command\ListContexts</command>
<command>OCA\Tables\Command\ShowContext</command>
<command>OCA\Tables\Command\Clean</command>
<command>OCA\Tables\Command\CleanLegacy</command>
<command>OCA\Tables\Command\TransferLegacyRows</command>
Expand Down
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,6 @@
['name' => 'ApiColumns#createDatetimeColumn', 'url' => '/api/2/columns/datetime', 'verb' => 'POST'],

['name' => 'Context#index', 'url' => '/api/2/contexts', 'verb' => 'GET'],
['name' => 'Context#show', 'url' => '/api/2/contexts/{contextId}', 'verb' => 'GET'],
]
];
98 changes: 98 additions & 0 deletions lib/Command/ShowContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace OCA\Tables\Command;

use OC\Core\Command\Base;
use OCA\Tables\Errors\InternalError;
use OCA\Tables\Service\ContextService;
use OCP\DB\Exception;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function json_decode;
use function json_encode;

class ShowContext extends Base {
protected ContextService $contextService;
protected LoggerInterface $logger;
private IConfig $config;

public function __construct(
ContextService $contextService,
LoggerInterface $logger,
IConfig $config,
) {
parent::__construct();
$this->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('<error>Invalid Context ID</error>');
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('<warning>%s</warning>', $e->getMessage()));
$output->writeln('<error>');
debug_print_backtrace();
$output->writeln('</error>');
}
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;
}
}
19 changes: 19 additions & 0 deletions lib/Controller/ContextController.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ public function index(): DataResponse {
}
}

/**
* [api v3] Get information about the requests context
*
* @return DataResponse<Http::STATUS_OK, TablesContext, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 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 (InternalError|Exception $e) {
return $this->handleError($e);
}
}

/**
* @param Context[] $contexts
* @return array
Expand Down
22 changes: 21 additions & 1 deletion lib/Db/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,46 @@
* @method setOwnerId(string $value): void
* @method getOwnerType(): int
* @method setOwnerType(int $value): void
*
* @method getSharing(): array
* @method setSharing(array $value): void
* @method getNodes(): array
* @method setNodes(array $value): void
* @method getPages(): array
* @method setPages(array $value): void
*/
class Context extends Entity implements JsonSerializable {
protected ?string $name = null;
protected ?string $icon = null;
protected ?string $description = null;
protected ?string $ownerId = null;
protected ?int $ownerType = null;
protected ?array $sharing = null;
protected ?array $nodes = null;
protected ?array $pages = null;

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

public function jsonSerialize(): array {
return [
// 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;
}
}
149 changes: 124 additions & 25 deletions lib/Db/ContextMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace OCA\Tables\Db;

use OCA\Tables\Helper\UserHelper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
Expand All @@ -19,7 +21,6 @@ public function __construct(IDBConnection $db, UserHelper $userHelper) {
$this->userHelper = $userHelper;
parent::__construct($db, $this->table, Context::class);
}

/**
* @return Context[]
* @throws Exception
Expand All @@ -29,38 +30,136 @@ public function findAll(?string $userId = null): array {
$qb->select('c.*')
->from($this->table, 'c');
if ($userId !== null) {
$sharedToConditions = $qb->expr()->orX();
$this->applyOwnedOrSharedQuery($qb, $userId);
}

// 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);
}
return $this->findEntities($qb);
}

/**
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
* @throws Exception
*/
public function findById(int $contextId, ?string $userId = null): Context {
$qb = $this->db->getQueryBuilder();
$qb->select(
'c.*',
'r.id as node_rel_id', 'r.node_id', 'r.node_type', 'r.permissions',
'p.page_type',
'pc.id as content_id', 'pc.page_id', 'pc.order',
'n.display_mode as display_mode_default',
's.id as share_id', 's.receiver', 's.receiver_type'
)
->from($this->table, 'c')
->where($qb->expr()->eq('c.id', $qb->createNamedParameter($contextId, IQueryBuilder::PARAM_INT)));

// owned contexts + apply share conditions
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')),
$sharedToConditions,
));
}

$qb->where($qb->expr()->orX(
$qb->expr()->eq('owner_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)),
$qb->expr()->isNotNull('s.receiver'),
));
$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()->eq('pc.node_rel_id', 'r.id'));
$qb->orderBy('pc.order', 'ASC');

$result = $qb->executeQuery();
$r = $result->fetchAll();

$formatted = [
'id' => $r[0]['id'],
'name' => $r[0]['name'],
'icon' => $r[0]['icon'],
'description' => $r[0]['description'],
'owner_id' => $r[0]['owner_type'],
];

$formatted['sharing'] = array_reduce($r, function (array $carry, array $item) use ($userId) {
$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($r, function (array $carry, array $item) {
$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($r, function (array $carry, array $item) {
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'];
$carry[$item['page_id']]['content'][$item['content_id']] = [
'order' => $item['order'],
'node_rel_id' => $item['node_rel_id']
];

return $carry;
}, []);

return $this->mapRowToEntity($formatted);
}

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);
}

return $this->findEntities($qb);
// 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,
));

$qb->where($qb->expr()->orX(
$qb->expr()->eq('owner_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)),
$qb->expr()->isNotNull('s.receiver'),
));
}
}
17 changes: 17 additions & 0 deletions lib/Service/ContextService.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,21 @@ public function findAll(?string $userId): array {
}
return $this->mapper->findAll($userId);
}

/**
* @return Context
* @throws InternalError
*/
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->mapper->findById($id, $userId);
}
}

0 comments on commit a9086d2

Please sign in to comment.