CVE-2026-45545
Description
Authenticated users of Nextcloud Tables app can inject SQL to read or modify database data.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Authenticated users of Nextcloud Tables app can inject SQL to read or modify database data.
Vulnerability
An SQL injection vulnerability exists in the Nextcloud Tables app, affecting versions 0.7.0 to before 0.7.7, 0.8.0 to before 0.8.10, 0.9.0 to before 0.9.8, and 1.0.0 to before 1.0.4. This flaw allows an authenticated attacker with access to the Tables app to execute arbitrary SQL queries, initially limited to 20 bytes, but bypassable with crafted input [1].
Exploitation
An attacker must be authenticated and have access to the Nextcloud Tables app. By providing carefully crafted input, they can exploit a stored injection vulnerability to execute SQL queries against the application's database. It is possible to bypass the initial 20-byte query length limitation [1].
Impact
Successful exploitation allows an attacker to execute arbitrary SQL queries, enabling them to extract sensitive information from the database or modify existing data. The scope of the compromise is limited to the data accessible by the application's database user [1].
Mitigation
The vulnerability has been patched in Nextcloud Tables app versions 0.7.7, 0.8.10, 0.9.8, 1.0.4, and 2.0.0. Users are recommended to upgrade to one of these patched versions. As a workaround, the Tables app can be disabled [1].
AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
15c30527f8c2dMerge pull request #2309 from nextcloud/enh/noid/view-update-input
24 files changed · +795 −271
lib/Constants/ColumnType.php+18 −0 added@@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Constants; + +enum ColumnType: string { + case NUMBER = 'number'; + case TEXT = 'text'; + case SELECTION = 'selection'; + case DATETIME = 'datetime'; + case PEOPLE = 'usergroup'; +}
lib/Constants/FilterOperator.php+23 −0 added@@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Constants; + +enum FilterOperator: string { + case BEGINS_WITH = 'begins-with'; + case ENDS_WITH = 'ends-with'; + case CONTAINS = 'contains'; + case DOES_NOT_CONTAIN = 'does-not-contain'; + case IS_EQUAL = 'is-equal'; + case IS_NOT_EQUAL = 'is-not-equal'; + case IS_GREATER_THAN = 'is-greater-than'; + case IS_GREATER_THAN_OR_EQUAL = 'is-greater-than-or-equal'; + case IS_LESS_THAN = 'is-lower-than'; + case IS_LESS_THAN_OR_EQUAL = 'is-lower-than-or-equal'; + case IS_EMPTY = 'is-empty'; +}
lib/Constants/ViewUpdatableParameters.php+19 −0 added@@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Constants; + +enum ViewUpdatableParameters: string { + case TITLE = 'title'; + case EMOJI = 'emoji'; + case DESCRIPTION = 'description'; + case SORT = 'sort'; + case FILTER = 'filter'; + case COLUMN_SETTINGS = 'columns'; +}
lib/Controller/Api1Controller.php+28 −6 modified@@ -13,13 +13,15 @@ use InvalidArgumentException; use OCA\Tables\Api\V1Api; use OCA\Tables\AppInfo\Application; +use OCA\Tables\Constants\ColumnType; use OCA\Tables\Db\ViewMapper; use OCA\Tables\Dto\Column as ColumnDto; use OCA\Tables\Errors\BadRequestError; use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; use OCA\Tables\Middleware\Attribute\RequirePermission; +use OCA\Tables\Model\ViewUpdateInput; use OCA\Tables\ResponseDefinitions; use OCA\Tables\Service\ColumnService; use OCA\Tables\Service\ImportService; @@ -40,6 +42,7 @@ use OCP\IL10N; use OCP\IRequest; use Psr\Log\LoggerInterface; +use ValueError; /** * @psalm-import-type TablesTable from ResponseDefinitions @@ -394,7 +397,15 @@ public function getView(int $viewId): DataResponse { * Update a view via key-value sets * * @param int $viewId View ID - * @param array{key: 'title'|'emoji'|'description', value: string}|array{key: 'columns', value: list<int>}|array{key: 'sort', value: array{columnId: int, mode: 'ASC'|'DESC'}}|array{key: 'filter', value: array{columnId: int, operator: 'begins-with'|'ends-with'|'contains'|'does-not-contain'|'is-equal'|'is-not-equal'|'is-greater-than'|'is-greater-than-or-equal'|'is-lower-than'|'is-lower-than-or-equal'|'is-empty', value: string|int|float}} $data key-value pairs + * @param array{ + * title?: string, + * emoji?: string, + * description?: string, + * columns?: list<int>, + * columnSettings?: list<array{columnId?: int, order?: int, readonly?: bool, mandatory?: bool}>, + * sort?: list<array{columnId: int, mode: 'ASC'|'DESC'}>, + * filter?: list<list<array{columnId: int, operator: 'begins-with'|'ends-with'|'contains'|'does-not-contain'|'is-equal'|'is-not-equal'|'is-greater-than'|'is-greater-than-or-equal'|'is-lower-than'|'is-lower-than-or-equal'|'is-empty', value: string|int|float}>> + * } $data fields of the view with their new values * @return DataResponse<Http::STATUS_OK, TablesView, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND|Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> * * 200: View updated @@ -409,7 +420,8 @@ public function getView(int $viewId): DataResponse { #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function updateView(int $viewId, array $data): DataResponse { try { - return new DataResponse($this->viewService->update($viewId, $data)->jsonSerialize()); + $inputData = ViewUpdateInput::fromInputArray($data); + return new DataResponse($this->viewService->update($viewId, $inputData)->jsonSerialize()); } catch (PermissionError $e) { $this->logger->warning('A permission error occurred: ' . $e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; @@ -835,9 +847,10 @@ public function indexViewColumns(int $viewId): DataResponse { * @param list<int>|null $selectedViewIds View IDs where this column should be added to be presented * @param array<string, mixed> $customSettings Custom settings for the column * - * @return DataResponse<Http::STATUS_OK, TablesColumn, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * @return DataResponse<Http::STATUS_OK, TablesColumn, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND|Http::STATUS_BAD_REQUEST, array{message: string}, array{}> * * 200: Column created + * 400: Invalid input data * 403: No permissions * 404: Not found */ @@ -887,7 +900,7 @@ public function createColumn( $viewId, new ColumnDto( title: $title, - type: $type, + type: ColumnType::from($type)->value, subtype: $subtype, mandatory: $mandatory, description: $description, @@ -926,6 +939,10 @@ public function createColumn( $this->logger->info('A not found error occurred: ' . $e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); + } catch (ValueError $e) { + $this->logger->info('A invalid value was provided: ' . $e->getMessage(), ['exception' => $e]); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_BAD_REQUEST); } } @@ -1598,9 +1615,10 @@ public function createTableShare(int $tableId, string $receiver, string $receive * @param list<int>|null $selectedViewIds View IDs where this column should be added to be presented * @param array<string, mixed> $customSettings Custom settings for the column * - * @return DataResponse<Http::STATUS_OK, TablesColumn, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * @return DataResponse<Http::STATUS_OK, TablesColumn, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND|Http::STATUS_BAD_REQUEST, array{message: string}, array{}> * * 200: Column created + * 400: Bad request * 403: No permissions * 404: Not found */ @@ -1650,7 +1668,7 @@ public function createTableColumn( null, new ColumnDto( title: $title, - type: $type, + type: ColumnType::from($type)->value, subtype: $subtype, mandatory: $mandatory, description: $description, @@ -1690,6 +1708,10 @@ public function createTableColumn( $this->logger->info('A not found error occurred: ' . $e->getMessage(), ['exception' => $e]); $message = ['message' => $e->getMessage()]; return new DataResponse($message, Http::STATUS_NOT_FOUND); + } catch (ValueError $e) { + $this->logger->info('A invalid value was provided: ' . $e->getMessage(), ['exception' => $e]); + $message = ['message' => $e->getMessage()]; + return new DataResponse($message, Http::STATUS_BAD_REQUEST); } } }
lib/Controller/ApiColumnsController.php+6 −5 modified@@ -8,6 +8,7 @@ namespace OCA\Tables\Controller; use OCA\Tables\AppInfo\Application; +use OCA\Tables\Constants\ColumnType; use OCA\Tables\Db\Column; use OCA\Tables\Dto\Column as ColumnDto; use OCA\Tables\Errors\BadRequestError; @@ -151,7 +152,7 @@ public function createNumberColumn(int $baseNodeId, string $title, ?float $numbe $viewId, new ColumnDto( title: $title, - type: 'number', + type: ColumnType::NUMBER->value, subtype: $subtype, mandatory: $mandatory, description: $description, @@ -213,7 +214,7 @@ public function createTextColumn(int $baseNodeId, string $title, ?string $textDe $viewId, new ColumnDto( title: $title, - type: 'text', + type: ColumnType::TEXT->value, subtype: $subtype, mandatory: $mandatory, description: $description, @@ -275,7 +276,7 @@ public function createSelectionColumn(int $baseNodeId, string $title, string $se $viewId, new ColumnDto( title: $title, - type: 'selection', + type: ColumnType::SELECTION->value, subtype: $subtype, mandatory: $mandatory, description: $description, @@ -332,7 +333,7 @@ public function createDatetimeColumn(int $baseNodeId, string $title, ?string $da $viewId, new ColumnDto( title: $title, - type: 'datetime', + type: ColumnType::DATETIME->value, subtype: $subtype, mandatory: $mandatory, description: $description, @@ -391,7 +392,7 @@ public function createUsergroupColumn(int $baseNodeId, string $title, ?string $u $viewId, new ColumnDto( title: $title, - type: 'usergroup', + type: ColumnType::PEOPLE->value, mandatory: $mandatory, description: $description, usergroupDefault: $usergroupDefault,
lib/Controller/ApiTablesController.php+10 −7 modified@@ -14,6 +14,7 @@ use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; use OCA\Tables\Middleware\Attribute\RequirePermission; +use OCA\Tables\Model\ViewUpdateInput; use OCA\Tables\ResponseDefinitions; use OCA\Tables\Service\ColumnService; use OCA\Tables\Service\TableService; @@ -182,18 +183,19 @@ public function createFromScheme(string $title, string $emoji, string $descripti $this->userId, ); + $inputColumnsArray = []; if (isset($view['columnSettings'])) { $newColumns = array_map(static function (array $column) use ($colMap): array { $colId = $column['columnId']; $column['columnId'] = $colId > 0 ? $colMap[$colId] : $colId; return $column; }, $view['columnSettings']); - $columnModeKey = 'columnSettings'; + $inputColumnsArray['columnSettings'] = $newColumns; } else { $newColumns = array_map(static function (int $colId) use ($colMap): int { return $colId > 0 ? $colMap[$colId] : $colId; }, $view['columns']); - $columnModeKey = 'columns'; + $inputColumnsArray['columns'] = $newColumns; } $newSort = array_map(static function (array $sort) use ($colMap): array { @@ -212,11 +214,12 @@ public function createFromScheme(string $title, string $emoji, string $descripti }, $filters); }, $view['filter']); - $this->viewService->update($newView->getId(), [ - $columnModeKey => json_encode($newColumns), - 'sort' => json_encode($newSort), - 'filter' => json_encode($newFilter), - ]); + $this->viewService->update($newView->getId(), ViewUpdateInput::fromInputArray( + array_merge($inputColumnsArray, [ + 'sort' => $newSort, + 'filter' => $newFilter, + ]) + )); } $this->db->commit(); return new DataResponse($table->jsonSerialize());
lib/Controller/ViewController.php+3 −1 modified@@ -14,6 +14,7 @@ use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; use OCA\Tables\Middleware\Attribute\RequirePermission; +use OCA\Tables\Model\ViewUpdateInput; use OCA\Tables\Service\TableService; use OCA\Tables\Service\ViewService; use OCP\AppFramework\Controller; @@ -87,7 +88,8 @@ public function create(int $tableId, string $title, ?string $emoji): DataRespons #[RequirePermission(permission: Application::PERMISSION_MANAGE, type: Application::NODE_TYPE_VIEW, idParam: 'id')] public function update(int $id, array $data): DataResponse { return $this->handleError(function () use ($id, $data) { - return $this->service->update($id, $data, $this->userId); + $inputData = ViewUpdateInput::fromInputArray($data); + return $this->service->update($id, $inputData, $this->userId); }); }
lib/Db/Column.php+11 −1 modified@@ -9,9 +9,11 @@ use JsonSerializable; +use OCA\Tables\Constants\ColumnType; use OCA\Tables\Dto\Column as ColumnDto; use OCA\Tables\ResponseDefinitions; use OCA\Tables\Service\ValueObject\ViewColumnInformation; +use ValueError; /** * @psalm-suppress PropertyNotSetInConstructor @@ -34,7 +36,6 @@ * @method setLastEditByDisplayName(string $displayName) * @method getLastEditAt(): string * @method setLastEditAt(string $lastEditAt) - * @method getType(): string * @method setType(string $type) * @method getSubtype(): string * @method setSubtype(string $subtype) @@ -58,6 +59,7 @@ * @method setTextDefault(?string $textDefault) * @method getTextAllowedPattern(): string * @method setTextAllowedPattern(?string $textAllowedPattern) + * @method getTextAllowedPattern(): ?string * @method getTextMaxLength(): int * @method setTextMaxLength(?int $textMaxLength) * @method getTextUnique(): bool @@ -66,6 +68,7 @@ * @method getSelectionDefault(): string * @method setSelectionOptions(?string $selectionOptionsArray) * @method setSelectionDefault(?string $selectionDefault) + * @method getSelectionDefault(): ?string * @method getDatetimeDefault(): string * @method setDatetimeDefault(?string $datetimeDefault) * @method getUsergroupDefault(): string @@ -308,4 +311,11 @@ public function jsonSerialize(): array { public function getCustomSettingsArray(): array { return json_decode($this->customSettings, true) ?: []; } + + /** + * @throws ValueError + */ + public function getType(): string { + return ColumnType::from($this->type)->value; + } }
lib/Db/View.php+8 −13 modified@@ -10,7 +10,9 @@ namespace OCA\Tables\Db; use JsonSerializable; +use OCA\Tables\Model\FilterSet; use OCA\Tables\Model\Permissions; +use OCA\Tables\Model\SortRuleSet; use OCA\Tables\ResponseDefinitions; use OCA\Tables\Service\ValueObject\ViewColumnInformation; @@ -136,25 +138,17 @@ public function findColumnSettingsForColumn(int $columnId): ?ViewColumnInformati * @return list<array{columnId: int, mode: 'ASC'|'DESC'}> */ public function getSortArray(): array { - return $this->getArray($this->getSort()); + $rawSortRules = $this->getArray($this->getSort()); + return SortRuleSet::createFromInputArray($rawSortRules)->jsonSerialize(); } /** * @psalm-suppress MismatchingDocblockReturnType * @return list<list<array{columnId: int, operator: 'begins-with'|'ends-with'|'contains'|'does-not-contain'|'is-equal'|'is-not-equal'|'is-greater-than'|'is-greater-than-or-equal'|'is-lower-than'|'is-lower-than-or-equal'|'is-empty', value: string|int|float}>> */ public function getFilterArray():array { - $filters = $this->getArray($this->getFilter()); - // a filter(group) was stored with a not-selected column - it may break impressively. - // filter them out now until we have a permanent fix - foreach ($filters as &$filterGroups) { - $filterGroups = array_filter($filterGroups, function (array $item) { - return $item['columnId'] !== null; - }); - } - return array_filter($filters, function (array $item) { - return !empty($item); - }); + $rawFilters = $this->getArray($this->getFilter()); + return FilterSet::createFromInputArray($rawFilters)->jsonSerialize(); } private function getArray(?string $json): array { @@ -217,6 +211,7 @@ public function jsonSerialize(): array { */ public function getColumnIds(): array { $columns = $this->getColumnsSettingsArray(); - return array_map(static fn (ViewColumnInformation $column): int => $column[ViewColumnInformation::KEY_ID], $columns); + + return array_map(static fn (ViewColumnInformation $column): int => $column->getId(), $columns); } }
lib/Model/ColumnSettings.php+47 −0 added@@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Model; + +use Generator; +use JsonSerializable; +use OCA\Tables\Service\ValueObject\ViewColumnInformation; + +class ColumnSettings implements JSONSerializable { + /** + * @param ViewColumnInformation[] $columnSettings + */ + public function __construct( + protected array $columnSettings, + ) { + foreach ($this->columnSettings as $columnSetting) { + if (!$columnSetting instanceof ViewColumnInformation) { + throw new \InvalidArgumentException('Provided column settings must be of type ViewColumnInformation'); + } + } + } + + public function columnInformation(): Generator { + foreach ($this->columnSettings as $columnInformation) { + yield $columnInformation; + } + } + + public static function createFromInputArray(array $inputColumnSettings): self { + $columnSettings = []; + foreach ($inputColumnSettings as $inputColumnSetting) { + $columnSettings[] = ViewColumnInformation::fromArray($inputColumnSetting); + } + return new self($columnSettings); + } + + public function jsonSerialize(): mixed { + return $this->columnSettings; + } +}
lib/Model/FilterGroup.php+55 −0 added@@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Model; + +use InvalidArgumentException; +use JsonSerializable; +use OCA\Tables\Constants\FilterOperator; +use OCA\Tables\Service\ValueObject\Filter; +use ValueError; + +class FilterGroup implements JsonSerializable { + + /** + * @param Filter[] $filters + */ + public function __construct( + protected array $filters, + ) { + foreach ($filters as $filter) { + if (!$filter instanceof Filter) { + throw new InvalidArgumentException('Provided filter must be an instance of Filter'); + } + } + } + + public static function createFromInputArray(array $data): self { + $filters = []; + foreach ($data as $filterInput) { + if (!isset($filterInput['columnId'], $filterInput['operator'], $filterInput['value'])) { + throw new InvalidArgumentException('Required input fields are missing'); + } + try { + $filters[] = new Filter( + (int)$filterInput['columnId'], + FilterOperator::from($filterInput['operator']), + $filterInput['value'], + ); + } catch (ValueError $e) { + throw new InvalidArgumentException('Invalid input data passed to Filter', 0, $e); + } + } + return new self($filters); + } + + public function jsonSerialize(): array { + return array_map(static fn (Filter $f) => $f->jsonSerialize(), $this->filters); + } +}
lib/Model/FilterSet.php+41 −0 added@@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Model; + +use InvalidArgumentException; +use JsonSerializable; + +class FilterSet implements JsonSerializable { + + /** + * @param FilterGroup[] $filterGroups + */ + public function __construct( + protected array $filterGroups, + ) { + foreach ($this->filterGroups as $filterGroup) { + if (!($filterGroup instanceof FilterGroup)) { + throw new InvalidArgumentException('Provided filterGroup must be an instance of FilterGroup'); + } + } + } + + public static function createFromInputArray(array $data): self { + $filterGroups = []; + foreach ($data as $inputFilterGroup) { + $filterGroups[] = FilterGroup::createFromInputArray($inputFilterGroup); + } + return new self($filterGroups); + } + + public function jsonSerialize(): array { + return array_map(static fn (FilterGroup $fg) => $fg->jsonSerialize(), $this->filterGroups); + } +}
lib/Model/SortRuleSet.php+53 −0 added@@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Model; + +use InvalidArgumentException; +use JsonSerializable; +use OCA\Tables\Service\ValueObject\SortRule; + +class SortRuleSet implements JsonSerializable { + /** + * @param SortRule[] $sortRules + * @throws InvalidArgumentException + */ + public function __construct( + protected array $sortRules, + ) { + foreach ($this->sortRules as $sortRule) { + if (!($sortRule instanceof SortRule)) { + throw new InvalidArgumentException('Provided sort rule must be an instance of SortRule'); + } + } + } + + /** + * @param list<array{columnId: int, mode: 'ASC'|'DESC'}> $data + * @throws InvalidArgumentException + */ + public static function createFromInputArray(array $data): self { + $sortRules = []; + foreach ($data as $inputSortRule) { + if (!isset($inputSortRule['columnId'], $inputSortRule['mode'])) { + throw new InvalidArgumentException('Required sort parameters are missing'); + } + + $sortRules[] = new SortRule( + columnId: (int)$inputSortRule['columnId'], + mode: (string)$inputSortRule['mode'] + ); + } + return new self($sortRules); + } + + public function jsonSerialize(): array { + return array_map(static fn (SortRule $s) => $s->jsonSerialize(), $this->sortRules); + } +}
lib/Model/ViewUpdateInput.php+106 −0 added@@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Model; + +use Generator; +use OCA\Tables\AppInfo\Application; +use OCA\Tables\Constants\ViewUpdatableParameters; +use OCA\Tables\Service\ValueObject\Emoji; +use OCA\Tables\Service\ValueObject\Title; +use OCA\Tables\Service\ValueObject\ViewColumnInformation; +use OCP\Server; +use Psr\Log\LoggerInterface; +use function json_encode; + +class ViewUpdateInput { + protected ?array $sort = null; + + public function __construct( + protected readonly ?Title $title = null, + protected readonly ?string $description = null, + protected readonly ?Emoji $emoji = null, + protected readonly ?ColumnSettings $columnSettings = null, + protected readonly ?FilterSet $filterSet = null, + protected readonly ?SortRuleSet $sortRuleSet = null, + ) { + } + + public function updateDetail(): Generator { + if ($this->title) { + yield ViewUpdatableParameters::TITLE => $this->title; + } + if ($this->description) { + yield ViewUpdatableParameters::DESCRIPTION => $this->description; + } + if ($this->emoji) { + yield ViewUpdatableParameters::EMOJI => $this->emoji; + } + if ($this->columnSettings) { + yield ViewUpdatableParameters::COLUMN_SETTINGS => $this->columnSettings; + } + if ($this->filterSet) { + yield ViewUpdatableParameters::FILTER => $this->filterSet; + } + if ($this->sortRuleSet) { + yield ViewUpdatableParameters::SORT => $this->sortRuleSet; + } + } + + /** + * @param array{ + * title?: string, + * emoji?: string, + * description?: string, + * columns?: list<int>, + * columnSettings?: list<array{columnId?: int, order?: int, readonly?: bool, mandatory?: bool}>, + * sort?: list<array{columnId: int, mode: 'ASC'|'DESC'}>, + * filter?: list<list<array{columnId: int, operator: 'begins-with'|'ends-with'|'contains'|'does-not-contain'|'is-equal'|'is-not-equal'|'is-greater-than'|'is-greater-than-or-equal'|'is-lower-than'|'is-lower-than-or-equal'|'is-empty', value: string|int|float}>> + * } $data + */ + public static function fromInputArray(array $data): self { + $data = self::transformJsonToArrayInPayload($data, ['columnSettings', 'filter', 'sort']); + + if (isset($data['columns']) && !isset($data['columnSettings'])) { + $logger = Server::get(LoggerInterface::class); + $logger->info('The old columns format is deprecated. Please use the new format with columnId and order properties.', ['app' => Application::APP_ID]); + + $value = []; + foreach ($data['columns'] as $order => $columnId) { + $value[] = new ViewColumnInformation($columnId, order: $order); + } + $value = json_encode($value); + + $data['columnSettings'] = $value; + } + + return new self( + title: $data['title'] ? new Title($data['title']) : null, + description: $data['description'] ?? null, + emoji: $data['emoji'] ? new Emoji($data['emoji']) : null, + columnSettings: $data['columnSettings'] ? ColumnSettings::createFromInputArray($data['columnSettings']) : null, + filterSet: $data['filter'] ? FilterSet::createFromInputArray($data['filter']) : null, + sortRuleSet: $data['sort'] ? SortRuleSet::createFromInputArray($data['sort']) : null, + ); + } + + protected static function transformJsonToArrayInPayload(array $input, array $keys): array { + $output = $input; + foreach ($keys as $targetKey) { + if (!isset($input[$targetKey]) || !is_string($input[$targetKey])) { + continue; + } + $decoded = \json_decode($input[$targetKey], true); + if (is_array($decoded)) { + $output[$targetKey] = $decoded; + } + } + return $output; + } +}
lib/Service/RowService.php+1 −2 modified@@ -231,7 +231,6 @@ public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): R $data = $this->cleanupAndValidateData($data, $columns, $tableId, $viewId); $data = $this->enhanceWithViewDefaults($view, $data); - $tableId = $tableId ?? $view->getTableId(); $row2 = new Row2(); $row2->setTableId($tableId); $row2->setData($data); @@ -469,7 +468,7 @@ private function isValueValidForMandatoryColumn($value, Column $column, IColumnT $defaultValue = $column->getSelectionDefault(); return $defaultValue !== null && $defaultValue !== '' && $defaultValue !== '[]'; } - return $value !== null && $value !== '' && $value !== []; + return $value !== []; } /**
lib/Service/TableTemplateService.php+28 −36 modified@@ -16,7 +16,7 @@ use OCA\Tables\Errors\InternalError; use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; -use OCA\Tables\Service\ValueObject\ViewColumnInformation; +use OCA\Tables\Model\ViewUpdateInput; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\DB\Exception; @@ -65,7 +65,7 @@ public function getTemplateList(): array { [ 'name' => 'vacation-requests', 'title' => $this->l->t('Vacation requests'), - 'icon' => '️🏝', + 'icon' => '🏝', 'description' => $this->l->t('Use this table to collect and manage vacation requests.') ], [ @@ -451,7 +451,6 @@ private function makeVacationRequests(Table $table):void { $columns['workingDays']->getId() => 34, $columns['dateRequest']->getId() => '2023-01-30', $columns['approved']->getId() => 'false', - $columns['dateApprove']->getId() => '', $columns['approveBy']->getId() => '', // TRANSLATORS This is an example comment $columns['comment']->getId() => $this->l->t('We have to talk about that.'), @@ -469,54 +468,53 @@ private function makeVacationRequests(Table $table):void { $this->createView($table, [ 'title' => $this->l->t('Create Vacation Request'), - 'emoji' => '️➕', - 'columnSettings' => $this->columnsToColumnSettingsJsonString($columns), - 'filter' => json_encode([[['columnId' => Column::TYPE_META_CREATED_BY, 'operator' => 'is-equal', 'value' => '@my-name'], ['columnId' => $columns['approved']->getId(), 'operator' => 'is-empty', 'value' => '']]]), + 'emoji' => '➕', + 'columnSettings' => $this->columnsToInputArray($columns), + 'filter' => [[['columnId' => Column::TYPE_META_CREATED_BY, 'operator' => 'is-equal', 'value' => '@my-name'], ['columnId' => $columns['approved']->getId(), 'operator' => 'is-empty', 'value' => '']]], ] ); $this->createView($table, [ 'title' => $this->l->t('Open Request'), - 'emoji' => '️📝', - 'columnSettings' => $this->columnsToColumnSettingsJsonString($columns), - 'sort' => json_encode([['columnId' => $columns['from']->getId(), 'mode' => 'ASC']]), - 'filter' => json_encode([[['columnId' => $columns['approved']->getId(), 'operator' => 'is-empty', 'value' => '']]]), + 'emoji' => '📝', + 'columnSettings' => $this->columnsToInputArray($columns), + 'sort' => [['columnId' => $columns['from']->getId(), 'mode' => 'ASC']], + 'filter' => [[['columnId' => $columns['approved']->getId(), 'operator' => 'is-empty', 'value' => '']]], ] ); $this->createView($table, [ 'title' => $this->l->t('Request Status'), - 'emoji' => '️❓', - 'columnSettings' => $this->columnsToColumnSettingsJsonString($columns), - 'sort' => json_encode([['columnId' => Column::TYPE_META_UPDATED_BY, 'mode' => 'ASC']]), - 'filter' => json_encode([[['columnId' => Column::TYPE_META_CREATED_BY, 'operator' => 'is-equal', 'value' => '@my-name']]]), + 'emoji' => '❓', + 'columnSettings' => $this->columnsToInputArray($columns), + 'sort' => [['columnId' => Column::TYPE_META_UPDATED_BY, 'mode' => 'ASC']], + 'filter' => [[['columnId' => Column::TYPE_META_CREATED_BY, 'operator' => 'is-equal', 'value' => '@my-name']]], ] ); $this->createView($table, [ 'title' => $this->l->t('Closed requests'), - 'emoji' => '️✅', - 'columnSettings' => $this->columnsToColumnSettingsJsonString($columns), - 'sort' => json_encode([['columnId' => Column::TYPE_META_UPDATED_BY, 'mode' => 'ASC']]), - 'filter' => json_encode([[['columnId' => $columns['approved']->getId(), 'operator' => 'is-equal', 'value' => '@checked']], [['columnId' => $columns['approved']->getId(), 'operator' => 'is-equal', 'value' => '@unchecked']]]), + 'emoji' => '✅', + 'columnSettings' => $this->columnsToInputArray($columns), + 'sort' => [['columnId' => Column::TYPE_META_UPDATED_BY, 'mode' => 'ASC']], + 'filter' => [[['columnId' => $columns['approved']->getId(), 'operator' => 'is-equal', 'value' => '@checked']], [['columnId' => $columns['approved']->getId(), 'operator' => 'is-equal', 'value' => '@unchecked']]], ] ); } /** * @param array<?Column> $columns + * @return list<array{columnId: int, order: int}> */ - private function columnsToColumnSettingsJsonString(array $columns): string { + private function columnsToInputArray(array $columns): array { $columns = array_filter($columns, static function ($item) { return $item instanceof Column; }); - return json_encode( - array_map( - static function (Column $column, int $index): ViewColumnInformation { - return new ViewColumnInformation($column->getId(), order: $index); - }, - array_values($columns), array_keys(array_values($columns)) - ) + return array_map( + static function (Column $column, int $index): array { + return ['columnId' => $column->getId(), 'order' => $index]; + }, + array_values($columns), array_keys(array_values($columns)) ); } @@ -797,15 +795,8 @@ private function makeStartupTable(Table $table):void { $this->createView($table, [ 'title' => $this->l->t('Check yourself!'), 'emoji' => '🏁', - 'columnSettings' => json_encode( - [ - new ViewColumnInformation($columns['what']->getId(), order: 0), - new ViewColumnInformation($columns['how']->getId(), order: 1), - new ViewColumnInformation($columns['ease']->getId(), order: 2), - new ViewColumnInformation($columns['done']->getId(), order: 3), - ] - ), - 'filter' => json_encode([[['columnId' => $columns['done']->getId(), 'operator' => 'is-equal', 'value' => '@unchecked']]]), + 'columnSettings' => $this->columnsToInputArray($columns), + 'filter' => [[['columnId' => $columns['done']->getId(), 'operator' => 'is-equal', 'value' => '@unchecked']]], ]); } @@ -864,8 +855,9 @@ private function createRow(Table $table, array $values): void { */ private function createView(Table $table, array $data): void { try { + $inputData = ViewUpdateInput::fromInputArray($data); $view = $this->viewService->create($data['title'], $data['emoji'], $table); - $this->viewService->update($view->getId(), $data); + $this->viewService->update($view->getId(), $inputData); } catch (PermissionError $e) { $this->logger->warning('Cannot create view, permission denied: ' . $e->getMessage()); }
lib/Service/ValueObject/Emoji.php+31 −0 added@@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Service\ValueObject; + +use InvalidArgumentException; +use OCP\IEmojiHelper; +use OCP\Server; +use Stringable; + +class Emoji implements Stringable { + public function __construct( + protected string $emoji, + ) { + $this->emoji = trim($this->emoji); + $validator = Server::get(IEmojiHelper::class); + if (!$validator->isValidSingleEmoji($this->emoji)) { + throw new InvalidArgumentException('Only a single, valid emoji may be passed'); + } + } + + public function __toString(): string { + return $this->emoji; + } +}
lib/Service/ValueObject/Filter.php+30 −0 added@@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Service\ValueObject; + +use JsonSerializable; +use OCA\Tables\Constants\FilterOperator; + +class Filter implements JsonSerializable { + public function __construct( + protected readonly int $columnId, + protected readonly FilterOperator $operator, + protected readonly string $value, + ) { + } + + public function jsonSerialize(): array { + return [ + 'columnId' => $this->columnId, + 'operator' => $this->operator->value, + 'value' => $this->value, + ]; + } +}
lib/Service/ValueObject/SortRule.php+31 −0 added@@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Service\ValueObject; + +use InvalidArgumentException; +use JsonSerializable; + +class SortRule implements JsonSerializable { + public function __construct( + protected int $columnId, + protected string $mode, + ) { + if (!in_array($mode, ['ASC', 'DESC'], true)) { + throw new InvalidArgumentException('Invalid sort mode provided, ASC or DESC are expected'); + } + } + + public function jsonSerialize(): array { + return [ + 'columnId' => $this->columnId, + 'mode' => $this->mode, + ]; + } +}
lib/Service/ValueObject/Title.php+26 −0 added@@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Tables\Service\ValueObject; + +use Stringable; + +class Title implements Stringable { + public function __construct( + protected string $title, + ) { + if (strlen($this->title) > 200) { + throw new \InvalidArgumentException('Title exceed maximum length of 200 bytes'); + } + } + + public function __toString(): string { + return $this->title; + } +}
lib/Service/ValueObject/ViewColumnInformation.php+13 −10 modified@@ -12,16 +12,16 @@ use JsonSerializable; /** - * @template-implements ArrayAccess<string, mixed> + * @template-implements ArrayAccess<string, bool|int> */ class ViewColumnInformation implements ArrayAccess, JsonSerializable { public const KEY_ID = 'columnId'; public const KEY_ORDER = 'order'; public const KEY_READONLY = 'readonly'; public const KEY_MANDATORY = 'mandatory'; - /** @var array{columndId?: int, order?: int, readonly?: bool, mandatory?: bool} */ - protected array $data = []; + /** @var array{columnId: int, order: int, readonly?: bool, mandatory?: bool} */ + protected array $data; protected const KEYS = [ self::KEY_ID, self::KEY_ORDER, @@ -42,19 +42,19 @@ public function __construct( } public function getId(): int { - return $this->offsetGet(self::KEY_ID); + return (int)$this->offsetGet(self::KEY_ID); } public function getOrder(): int { - return $this->offsetGet(self::KEY_ORDER); + return (int)$this->offsetGet(self::KEY_ORDER); } public function isReadonly(): bool { - return $this->offsetGet(self::KEY_READONLY) ?? false; + return (bool)$this->offsetGet(self::KEY_READONLY); } public function isMandatory(): bool { - return $this->offsetGet(self::KEY_MANDATORY) ?? false; + return (bool)$this->offsetGet(self::KEY_MANDATORY); } public static function fromArray(array $data): static { @@ -72,8 +72,8 @@ public function offsetExists(mixed $offset): bool { return in_array((string)$offset, self::KEYS); } - public function offsetGet(mixed $offset): mixed { - return $this->data[$offset] ?? null; + public function offsetGet(mixed $offset): bool|int { + return $this->data[$offset]; } public function offsetSet(mixed $offset, mixed $value): void { @@ -91,11 +91,14 @@ public function offsetUnset(mixed $offset): void { unset($this->data[(string)$offset]); } + /** + * @return array{columnId: int, order: int, readonly?: bool, mandatory?: bool} + */ public function jsonSerialize(): array { return $this->data; } - protected function ensureType(string $offset, mixed $value): mixed { + protected function ensureType(string $offset, mixed $value): int|bool { return match ($offset) { self::KEY_ID, self::KEY_ORDER => (int)$value,
lib/Service/ViewService.php+74 −90 modified@@ -12,7 +12,9 @@ use DateTime; use Exception; use InvalidArgumentException; +use JsonSerializable; use OCA\Tables\AppInfo\Application; +use OCA\Tables\Constants\ViewUpdatableParameters; use OCA\Tables\Db\Column; use OCA\Tables\Db\Table; use OCA\Tables\Db\View; @@ -22,7 +24,11 @@ use OCA\Tables\Errors\PermissionError; use OCA\Tables\Event\ViewDeletedEvent; use OCA\Tables\Helper\UserHelper; +use OCA\Tables\Model\ColumnSettings; +use OCA\Tables\Model\FilterSet; use OCA\Tables\Model\Permissions; +use OCA\Tables\Model\SortRuleSet; +use OCA\Tables\Model\ViewUpdateInput; use OCA\Tables\ResponseDefinitions; use OCA\Tables\Service\ValueObject\ViewColumnInformation; use OCP\AppFramework\Db\DoesNotExistException; @@ -228,30 +234,12 @@ public function create(string $title, ?string $emoji, Table $table, ?string $use return $newItem; } - - /** - * @param int $id - * @param string $key - * @param string|null $value - * @param string|null $userId - * @return View - * @throws InternalError - */ - public function updateSingle(int $id, string $key, ?string $value, ?string $userId = null): View { - return $this->update($id, [$key => $value], $userId); - } - /** - * @param int $id - * @param array $data - * @param string|null $userId - * @param bool $skipTableEnhancement - * @return View * @throws InternalError * @throws PermissionError * @throws InvalidArgumentException */ - public function update(int $id, array $data, ?string $userId = null, bool $skipTableEnhancement = false): View { + public function update(int $id, ViewUpdateInput $data, ?string $userId = null, bool $skipTableEnhancement = false): View { $userId = $this->permissionsService->preCheckUserId($userId); try { @@ -262,52 +250,21 @@ public function update(int $id, array $data, ?string $userId = null, bool $skipT throw new PermissionError('PermissionError: can not update view with id ' . $id); } - $updatableParameter = ['title', 'emoji', 'description', 'sort', 'filter', 'columns', 'columnSettings']; - - foreach ($data as $key => $value) { - if (!in_array($key, $updatableParameter)) { - throw new InternalError('View parameter ' . $key . ' can not be updated.'); - } - - if ($key === 'columns') { - $this->logger->info('The old columns format is deprecated. Please use the new format with columnId and order properties.'); - $decodedValue = \json_decode($value, true); - $value = []; - foreach ($decodedValue as $order => $columnId) { - $value[] = new ViewColumnInformation($columnId, order: $order); - } - - $value = \json_encode($value); + foreach ($data->updateDetail() as $parameter => $value) { + if ($parameter === ViewUpdatableParameters::COLUMN_SETTINGS + && $value instanceof ColumnSettings + ) { + $this->assertInputColumnsAreValid($view, $userId, $value); } - if ($key === 'columnSettings' || $key === 'columns') { - // we have to fetch the service here as ColumnService already depends on the ViewService, i.e. no DI - $columnService = \OCP\Server::get(ColumnService::class); - $rawColumnsArray = \json_decode($value, true); - $columnIds = array_column($rawColumnsArray, ViewColumnInformation::KEY_ID); - - $availableColumns = $columnService->findAllByManagedView($view, $userId); - $availableColumns = array_map(static fn (Column $column) => $column->getId(), $availableColumns); - foreach ($columnIds as $columnId) { - if (!Column::isValidMetaTypeId($columnId) && !in_array($columnId, $availableColumns, true)) { - throw new InvalidArgumentException('Invalid column ID provided'); - } - } - - // ensure we have the correct format and expected values - try { - $columnsArray = array_map(static fn (array $a): ViewColumnInformation => ViewColumnInformation::fromArray($a), $rawColumnsArray); - $value = \json_encode($columnsArray); - } catch (\Throwable $t) { - throw new \InvalidArgumentException('Invalid column data provided', 400, $t); - } - - $key = 'columns'; + if ($value instanceof JsonSerializable) { + $insertableValue = json_encode($value); } - $setterMethod = 'set' . ucfirst($key); - $view->$setterMethod($value); + $setterMethod = 'set' . ucfirst($parameter->value); + $view->$setterMethod($insertableValue ?? $value); } + $time = new DateTime(); $view->setLastEditBy($userId); $view->setLastEditAt($time->format('Y-m-d H:i:s')); @@ -324,6 +281,24 @@ public function update(int $id, array $data, ?string $userId = null, bool $skipT } } + /** + * @throws InvalidArgumentException + * @throws PermissionError + */ + protected function assertInputColumnsAreValid(View $view, string $userId, ColumnSettings $columnSettings): void { + $columnService = \OCP\Server::get(ColumnService::class); + $availableColumns = $columnService->findAllByManagedView($view, $userId); + $availableColumnIds = array_map(static fn (Column $column) => $column->getId(), $availableColumns); + + foreach ($columnSettings->columnInformation() as $columnInfo) { + if (!in_array($columnInfo->getId(), $availableColumnIds, true) + && !Column::isValidMetaTypeId($columnInfo->getId()) + ) { + throw new InvalidArgumentException('Invalid column ID provided: ' . $columnInfo->getId()); + } + } + } + /** * @param int $id * @param string|null $userId @@ -527,48 +502,52 @@ public function deleteAllByTable(Table $table, ?string $userId = null): void { * @param Table $table * @return void * @throws InternalError + * @throws PermissionError */ - public function deleteColumnDataFromViews(int $columnId, Table $table) { + public function deleteColumnDataFromViews(int $columnId, Table $table): void { try { $views = $this->mapper->findAll($table->getId()); } catch (\OCP\DB\Exception $e) { throw new InternalError($e->getMessage()); } foreach ($views as $view) { - $filteredSortingRules = array_filter($view->getSortArray(), function (array $sort) use ($columnId) { + $filteredSortingRules = array_filter($view->getSortArray(), static function (array $sort) use ($columnId) { return $sort['columnId'] !== $columnId; }); $filteredSortingRules = array_values($filteredSortingRules); - $filteredFilters = array_filter( - array_map( - function (array $filterGroup) use ($columnId) { - return array_filter( - $filterGroup, - function (array $filter) use ($columnId) { - return $filter['columnId'] !== $columnId; - } - ); - }, - $view->getFilterArray() - ), - fn ($filterGroup) => !empty($filterGroup) + $applicableFilterArray = $this->removeColumnFromFilters($view->getFilterArray(), $columnId); + + $applicableViewColumnInformationRecords = array_filter( + $view->getColumnsSettingsArray(), + static function (ViewColumnInformation $viewColumnInformation) use ($columnId): bool { + return $viewColumnInformation->getId() !== $columnId; + } ); - $columnSettings = $view->getColumnsSettingsArray(); - $columnSettings = array_filter($columnSettings, static function (ViewColumnInformation $setting) use ($columnId): bool { - return $setting[ViewColumnInformation::KEY_ID] !== $columnId; - }); - $columnSettings = array_values($columnSettings); + $viewUpdateInput = new ViewUpdateInput( + columnSettings: new ColumnSettings($applicableViewColumnInformationRecords), + filterSet: FilterSet::createFromInputArray($applicableFilterArray), + sortRuleSet: SortRuleSet::createFromInputArray($filteredSortingRules), + ); - $data = [ - 'sort' => json_encode($filteredSortingRules), - 'filter' => json_encode($filteredFilters), - 'columnSettings' => json_encode($columnSettings), - ]; + $this->update($view->getId(), $viewUpdateInput); + } + } - $this->update($view->getId(), $data); + protected function removeColumnFromFilters(array $originalFilterSetArray, int $columnId): array { + $applicableFilterSetArray = []; + foreach ($originalFilterSetArray as $filterGroupArray) { + $currentGroup = []; + foreach ($filterGroupArray as $filterArray) { + if ($filterArray['columnId'] === $columnId) { + continue; + } + $currentGroup[] = $filterArray; + } + $applicableFilterSetArray[] = $currentGroup; } + return $applicableFilterSetArray; } /** @@ -603,11 +582,16 @@ public function search(string $term, int $limit = 100, int $offset = 0, ?string */ public function addColumnToView(View $view, Column $column, ?string $userId = null): void { try { - $columnsSettings = $view->getColumnsSettingsArray(); - $orders = array_map(fn (ViewColumnInformation $setting) => $setting->getOrder(), $view->getColumnsSettingsArray()); + $viewColumnInformation = $view->getColumnsSettingsArray(); + + $orders = array_map(fn (ViewColumnInformation $setting) => $setting->getOrder(), $viewColumnInformation); $nextOrder = $orders ? max($orders) + 1 : 0; - $columnsSettings[] = new ViewColumnInformation($column->getId(), $nextOrder); - $this->update($view->getId(), ['columnSettings' => json_encode($columnsSettings)], $userId, true); + + $viewColumnInformation[] = new ViewColumnInformation($column->getId(), $nextOrder); + $columnSettings = new ColumnSettings($viewColumnInformation); + + $viewUpdate = new ViewUpdateInput(columnSettings: $columnSettings); + $this->update($view->getId(), $viewUpdate, $userId, true); } catch (Exception $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError($e->getMessage());
openapi.json+94 −82 modified@@ -2071,99 +2071,75 @@ ], "properties": { "data": { - "description": "key-value pairs", - "anyOf": [ - { - "type": "object", - "required": [ - "key", - "value" - ], - "properties": { - "key": { - "type": "string", - "enum": [ - "title", - "emoji", - "description" - ] - }, - "value": { - "type": "string" - } + "type": "object", + "description": "fields of the view with their new values", + "properties": { + "title": { + "type": "string" + }, + "emoji": { + "type": "string" + }, + "description": { + "type": "string" + }, + "columns": { + "type": "array", + "items": { + "type": "integer", + "format": "int64" } }, - { - "type": "object", - "required": [ - "key", - "value" - ], - "properties": { - "key": { - "type": "string", - "enum": [ - "columns" - ] - }, - "value": { - "type": "array", - "items": { + "columnSettings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "columnId": { "type": "integer", "format": "int64" + }, + "order": { + "type": "integer", + "format": "int64" + }, + "readonly": { + "type": "boolean" + }, + "mandatory": { + "type": "boolean" } } } }, - { - "type": "object", - "required": [ - "key", - "value" - ], - "properties": { - "key": { - "type": "string", - "enum": [ - "sort" - ] - }, - "value": { - "type": "object", - "required": [ - "columnId", - "mode" - ], - "properties": { - "columnId": { - "type": "integer", - "format": "int64" - }, - "mode": { - "type": "string", - "enum": [ - "ASC", - "DESC" - ] - } + "sort": { + "type": "array", + "items": { + "type": "object", + "required": [ + "columnId", + "mode" + ], + "properties": { + "columnId": { + "type": "integer", + "format": "int64" + }, + "mode": { + "type": "string", + "enum": [ + "ASC", + "DESC" + ] } } } }, - { - "type": "object", - "required": [ - "key", - "value" - ], - "properties": { - "key": { - "type": "string", - "enum": [ - "filter" - ] - }, - "value": { + "filter": { + "type": "array", + "items": { + "type": "array", + "items": { "type": "object", "required": [ "columnId", @@ -2210,7 +2186,7 @@ } } } - ] + } } } } @@ -3888,6 +3864,24 @@ } } }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": { @@ -4287,6 +4281,24 @@ } } }, + "400": { + "description": "Invalid input data", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + }, "401": { "description": "Current user is not logged in", "content": {
src/types/openapi/openapi.ts+39 −18 modified@@ -1660,34 +1660,33 @@ export interface operations { readonly requestBody: { readonly content: { readonly "application/json": { - /** @description key-value pairs */ + /** @description fields of the view with their new values */ readonly data: { - /** @enum {string} */ - readonly key: "title" | "emoji" | "description"; - readonly value: string; - } | { - /** @enum {string} */ - readonly key: "columns"; - readonly value: readonly number[]; - } | { - /** @enum {string} */ - readonly key: "sort"; - readonly value: { + readonly title?: string; + readonly emoji?: string; + readonly description?: string; + readonly columns?: readonly number[]; + readonly columnSettings?: readonly { + /** Format: int64 */ + readonly columnId?: number; + /** Format: int64 */ + readonly order?: number; + readonly readonly?: boolean; + readonly mandatory?: boolean; + }[]; + readonly sort?: readonly { /** Format: int64 */ readonly columnId: number; /** @enum {string} */ readonly mode: "ASC" | "DESC"; - }; - } | { - /** @enum {string} */ - readonly key: "filter"; - readonly value: { + }[]; + readonly filter?: readonly (readonly { /** Format: int64 */ readonly columnId: number; /** @enum {string} */ readonly operator: "begins-with" | "ends-with" | "contains" | "does-not-contain" | "is-equal" | "is-not-equal" | "is-greater-than" | "is-greater-than-or-equal" | "is-lower-than" | "is-lower-than-or-equal" | "is-empty"; readonly value: string | number; - }; + }[])[]; }; }; }; @@ -2649,6 +2648,17 @@ export interface operations { readonly "application/json": components["schemas"]["Column"]; }; }; + /** @description Bad request */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly message: string; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: { @@ -2901,6 +2911,17 @@ export interface operations { readonly "application/json": components["schemas"]["Column"]; }; }; + /** @description Invalid input data */ + readonly 400: { + headers: { + readonly [name: string]: unknown; + }; + content: { + readonly "application/json": { + readonly message: string; + }; + }; + }; /** @description Current user is not logged in */ readonly 401: { headers: {
Vulnerability mechanics
No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.
References
3News mentions
0No linked articles in our index yet.