Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,5 @@
['name' => 'Context#destroy', 'url' => '/api/2/contexts/{contextId}', 'verb' => 'DELETE'],
['name' => 'Context#transfer', 'url' => '/api/2/contexts/{contextId}/transfer', 'verb' => 'PUT'],
['name' => 'Context#updateContentOrder', 'url' => '/api/2/contexts/{contextId}/pages/{pageId}', 'verb' => 'PUT'],

['name' => 'RowOCS#createRow', 'url' => '/api/2/{nodeCollection}/{nodeId}/rows', 'verb' => 'POST', 'requirements' => ['nodeCollection' => '(tables|views)', 'nodeId' => '(\d+)']],
]
];
19 changes: 19 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@
use OCA\Analytics\Datasource\DatasourceEvent;
use OCA\Circles\Events\CircleDestroyedEvent;
use OCA\Tables\Capabilities;
use OCA\Tables\Config\ConfigLexicon;
use OCA\Tables\Event\RowDeletedEvent;
use OCA\Tables\Event\TableDeletedEvent;
use OCA\Tables\Event\TableOwnershipTransferredEvent;
use OCA\Tables\Event\ViewDeletedEvent;
use OCA\Tables\Federation\FederationProvider;
use OCA\Tables\Listener\AddMissingIndicesListener;
use OCA\Tables\Listener\AnalyticsDatasourceListener;
use OCA\Tables\Listener\BeforeTemplateRenderedListener;
use OCA\Tables\Listener\LoadAdditionalListener;
use OCA\Tables\Listener\ReceiverCleanupListener;
use OCA\Tables\Listener\ResourceTypeRegisterListener;
use OCA\Tables\Listener\TablesReferenceListener;
use OCA\Tables\Listener\UserDeletedListener;
use OCA\Tables\Listener\WhenRowDeletedAuditLogListener;
Expand All @@ -42,7 +45,11 @@
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\Federation\ICloudFederationProvider;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\OCM\Events\LocalOCMDiscoveryEvent;
use OCP\Server;
use OCP\User\Events\BeforeUserDeletedEvent;
use OCP\User\Events\UserDeletedEvent;
use Psr\Container\ContainerInterface;
Expand Down Expand Up @@ -94,6 +101,7 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(UserDeletedEvent::class, ReceiverCleanupListener::class);
$context->registerEventListener(GroupDeletedEvent::class, ReceiverCleanupListener::class);
$context->registerEventListener(CircleDestroyedEvent::class, ReceiverCleanupListener::class);
$context->registerEventListener(LocalOCMDiscoveryEvent::class, ResourceTypeRegisterListener::class);

$context->registerSearchProvider(SearchTablesProvider::class);

Expand All @@ -106,8 +114,19 @@ public function register(IRegistrationContext $context): void {
$context->registerMiddleware(ShareControlMiddleware::class);

$context->registerUserMigrator(TablesMigrator::class);

$context->registerConfigLexicon(ConfigLexicon::class);
}

public function boot(IBootContext $context): void {
$context->injectFn([$this, 'registerCloudFederationProviderManager']);
}

public function registerCloudFederationProviderManager(ICloudFederationProviderManager $manager): void {
$manager->addCloudFederationProvider(
FederationProvider::PROVIDER_ID,
'Tables Federation',
static fn (): ICloudFederationProvider => Server::get(FederationProvider::class),
);
}
}
43 changes: 43 additions & 0 deletions lib/Config/ConfigLexicon.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Tables\Config;

use OCP\Config\Lexicon\Entry;
use OCP\Config\Lexicon\ILexicon;
use OCP\Config\Lexicon\Strictness;
use OCP\Config\ValueType;

/**
* Config Lexicon for tables.
*
* Please Add & Manage your Config Keys in that file and keep the Lexicon up to date!
*
* {@see ILexicon}
*/
class ConfigLexicon implements ILexicon {
public const FEDERATION_ENABLED = 'federationEnabled';

#[\Override]
public function getStrictness(): Strictness {
return Strictness::IGNORE;
}

#[\Override]
public function getAppConfigs(): array {
return [
new Entry(self::FEDERATION_ENABLED, ValueType::BOOL, true, 'Enable or disable federated table sharing'),
];
}

#[\Override]
public function getUserConfigs(): array {
return [];
}
}
1 change: 1 addition & 0 deletions lib/Constants/ShareReceiverType.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ class ShareReceiverType {
public const GROUP = 'group';
public const CIRCLE = 'circle';
public const LINK = 'link';
public const REMOTE = 'remote';
}
12 changes: 12 additions & 0 deletions lib/Controller/Api1Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use OCA\Tables\Model\ViewUpdateInput;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\ColumnService;
use OCA\Tables\Service\FederationService;
use OCA\Tables\Service\ImportService;
use OCA\Tables\Service\RelationService;
use OCA\Tables\Service\RowService;
Expand Down Expand Up @@ -88,6 +89,7 @@ public function __construct(
LoggerInterface $logger,
IL10N $l10N,
?string $userId,
private FederationService $federationService,
) {
parent::__construct(Application::APP_ID, $request);
$this->tableService = $service;
Expand Down Expand Up @@ -770,6 +772,11 @@ public function updateShareDisplayMode(int $shareId, int $displayMode, string $t
#[CORS]
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function indexTableColumns(int $tableId, ?int $viewId): DataResponse {
if ($this->federationService->isNodeFederated($tableId, 'table')) {
$table = $this->tableService->find($tableId, true);
return new DataResponse($this->federationService->getColumns($table));
}

try {
if ($viewId) {
$view = $this->viewService->find($viewId, false, $this->userId);
Expand Down Expand Up @@ -813,6 +820,11 @@ public function indexTableColumns(int $tableId, ?int $viewId): DataResponse {
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')]
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
public function indexViewColumns(int $viewId): DataResponse {
if ($this->federationService->isNodeFederated($viewId, 'view')) {
$view = $this->viewService->find($viewId, true);
return new DataResponse($this->federationService->getColumns($view));
}

try {
return new DataResponse($this->columnService->formatColumns($this->columnService->findAllByView($viewId)));
} catch (PermissionError $e) {
Expand Down
17 changes: 17 additions & 0 deletions lib/Controller/RowController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

use OCA\Tables\AppInfo\Application;
use OCA\Tables\Middleware\Attribute\RequirePermission;
use OCA\Tables\Service\FederationService;
use OCA\Tables\Service\RowService;
use OCA\Tables\Service\TableService;
use OCA\Tables\Service\ViewService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
Expand All @@ -24,13 +27,20 @@ public function __construct(
protected LoggerInterface $logger,
private RowService $service,
private ?string $userId,
private TableService $tableService,
private ViewService $viewService,
private FederationService $federationService,
) {
parent::__construct(Application::APP_ID, $request);
}

#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')]
public function index(int $tableId): DataResponse {
if ($this->federationService->isNodeFederated($tableId, 'table')) {
$table = $this->tableService->find($tableId, true);
return new DataResponse($this->federationService->getRows($table));
}
return $this->handleError(function () use ($tableId) {
return $this->service->findAllByTable($tableId, $this->userId);
});
Expand All @@ -39,6 +49,10 @@ public function index(int $tableId): DataResponse {
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_READ, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')]
public function indexView(int $viewId): DataResponse {
if ($this->federationService->isNodeFederated($viewId, 'view')) {
$view = $this->viewService->find($viewId, false, $this->userId);
return new DataResponse($this->federationService->getRows($view));
}
return $this->handleError(function () use ($viewId) {
return $this->service->findAllByView($viewId, $this->userId);
});
Expand Down Expand Up @@ -98,6 +112,9 @@ public function destroyByView(int $id, int $viewId): DataResponse {

#[NoAdminRequired]
public function presentInView(int $id, int $viewId): DataResponse {
if ($this->federationService->isNodeFederated($viewId, 'view')) {
return new DataResponse(['present' => true]);
}
return $this->handleError(function () use ($id, $viewId) {
$present = $this->service->isRowInViewPresent($id, $viewId, $this->userId);
return ['present' => $present];
Expand Down
109 changes: 109 additions & 0 deletions lib/Controller/RowOCSController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
use OCA\Tables\Middleware\Attribute\RequirePermission;
use OCA\Tables\Model\RowDataInput;
use OCA\Tables\ResponseDefinitions;
use OCA\Tables\Service\FederationService;
use OCA\Tables\Service\RowService;
use OCA\Tables\Service\TableService;
use OCA\Tables\Service\ViewService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\IL10N;
Expand All @@ -35,6 +39,9 @@ public function __construct(
IL10N $n,
string $userId,
protected RowService $rowService,
private TableService $tableService,
private ViewService $viewService,
private FederationService $federationService,
) {
parent::__construct($request, $logger, $n, $userId);
}
Expand All @@ -55,6 +62,7 @@ public function __construct(
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_CREATE, typeParam: 'nodeCollection')]
#[ApiRoute(verb: 'POST', url: '/api/2/{nodeCollection}/{nodeId}/rows', requirements: ['nodeCollection' => '(tables|views)', 'nodeId' => '(\d+)'])]
public function createRow(string $nodeCollection, int $nodeId, mixed $data): DataResponse {
if (is_string($data)) {
$data = json_decode($data, true);
Expand All @@ -67,8 +75,16 @@ public function createRow(string $nodeCollection, int $nodeId, mixed $data): Dat
$tableId = $viewId = null;
if ($iNodeType === Application::NODE_TYPE_TABLE) {
$tableId = $nodeId;
if ($this->federationService->isNodeFederated($tableId, 'table')) {
$table = $this->tableService->find($nodeId, true);
return new DataResponse($this->federationService->createRow($table, $data));
}
} elseif ($iNodeType === Application::NODE_TYPE_VIEW) {
$viewId = $nodeId;
if ($this->federationService->isNodeFederated($viewId, 'view')) {
$view = $this->viewService->find($nodeId, false, $this->userId);
return new DataResponse($this->federationService->createRow($view, $data));
}
}

$newRowData = new RowDataInput();
Expand All @@ -88,4 +104,97 @@ public function createRow(string $nodeCollection, int $nodeId, mixed $data): Dat
return $this->handleError($e);
}
}

/**
* [api v2] Update a row in a table or a view
*
* @param 'tables'|'views' $nodeCollection Indicates whether to update a row on a table or view
* @param int $nodeId The identifier of the targeted table or view
* @param int $rowId The identifier of the row to update
* @param string|array<string, mixed> $data An array containing the column identifiers and their values
* @return DataResponse<Http::STATUS_OK, TablesRow, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Row updated
* 403: No permissions
* 404: Not found
* 500: Internal error
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_UPDATE, typeParam: 'nodeCollection')]
#[ApiRoute(verb: 'PUT', url: '/api/2/{nodeCollection}/{nodeId}/rows/{rowId}', requirements: ['nodeCollection' => '(tables|views)', 'nodeId' => '(\d+)'])]
public function updateRow(string $nodeCollection, int $nodeId, int $rowId, mixed $data): DataResponse {
if (is_string($data)) {
$data = json_decode($data, true);
}
if (!is_array($data)) {
return $this->handleBadRequestError(new BadRequestError('Cannot update row: data input is invalid.'));
}
$iNodeType = ConversionHelper::stringNodeType2Const($nodeCollection);
$tableId = $viewId = null;
if ($iNodeType === Application::NODE_TYPE_TABLE) {
$tableId = $nodeId;
if ($this->federationService->isNodeFederated($tableId, 'table')) {
$table = $this->tableService->find($nodeId, true);
return new DataResponse($this->federationService->updateRow($table, $rowId, $data));
}
} elseif ($iNodeType === Application::NODE_TYPE_VIEW) {
$viewId = $nodeId;
if ($this->federationService->isNodeFederated($viewId, 'view')) {
$view = $this->viewService->find($nodeId, false, $this->userId);
return new DataResponse($this->federationService->updateRow($view, $rowId, $data));
}
}
try {
return new DataResponse($this->rowService->updateSet($rowId, $viewId, $data, $this->userId, $tableId)->jsonSerialize());
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (InternalError|\Exception $e) {
return $this->handleError($e);
}
}

/**
* [api v2] Delete a row in a table or a view
*
* @param 'tables'|'views' $nodeCollection Indicates whether to delete a row on a table or view
* @param int $nodeId The identifier of the targeted table or view
* @param int $rowId The identifier of the row to delete
* @return DataResponse<Http::STATUS_OK, TablesRow, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Row deleted
* 403: No permissions
* 404: Not found
* 500: Internal error
*/
#[NoAdminRequired]
#[RequirePermission(permission: Application::PERMISSION_DELETE, typeParam: 'nodeCollection')]
#[ApiRoute(verb: 'DELETE', url: '/api/2/{nodeCollection}/{nodeId}/rows/{rowId}', requirements: ['nodeCollection' => '(tables|views)', 'nodeId' => '(\d+)'])]
public function deleteRow(string $nodeCollection, int $nodeId, int $rowId): DataResponse {
$iNodeType = ConversionHelper::stringNodeType2Const($nodeCollection);
$tableId = $viewId = null;
if ($iNodeType === Application::NODE_TYPE_TABLE) {
$tableId = $nodeId;
if ($this->federationService->isNodeFederated($tableId, 'table')) {
$table = $this->tableService->find($nodeId, true);
return new DataResponse($this->federationService->deleteRow($table, $rowId));
}
} elseif ($iNodeType === Application::NODE_TYPE_VIEW) {
$viewId = $nodeId;
if ($this->federationService->isNodeFederated($viewId, 'view')) {
$view = $this->viewService->find($nodeId, false, $this->userId);
return new DataResponse($this->federationService->deleteRow($view, $rowId));
}
}
try {
return new DataResponse($this->rowService->delete($rowId, $viewId, $this->userId, $tableId)->jsonSerialize());
} catch (NotFoundError $e) {
return $this->handleNotFoundError($e);
} catch (PermissionError $e) {
return $this->handlePermissionError($e);
} catch (InternalError|\Exception $e) {
return $this->handleError($e);
}
}
}
15 changes: 15 additions & 0 deletions lib/Db/ShareMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace OCA\Tables\Db;

use OCA\Tables\Constants\ShareReceiverType;
use OCA\Tables\Service\ValueObject\ShareToken;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
Expand Down Expand Up @@ -261,4 +262,18 @@ public function deleteByReceiver(string $receiver, string $receiverType): int {
->andWhere($qb->expr()->eq('receiver_type', $qb->createNamedParameter($receiverType, IQueryBuilder::PARAM_STR)))
->executeStatement();
}

/**
* @return Share[]
* @throws Exception
*/
public function findRemoteSharesForNode(int $nodeId, string $nodeType): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->table)
->where($qb->expr()->eq('node_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('node_type', $qb->createNamedParameter($nodeType, IQueryBuilder::PARAM_STR)))
->andWhere($qb->expr()->eq('receiver_type', $qb->createNamedParameter(ShareReceiverType::REMOTE, IQueryBuilder::PARAM_STR)));
return $this->findEntities($qb);
}
}
Loading
Loading