Skip to content

Commit 8093d63

Browse files
committed
feat: add relation column type (improve security)
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent b463892 commit 8093d63

File tree

3 files changed

+36
-26
lines changed

3 files changed

+36
-26
lines changed

lib/Db/Column.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ class Column extends EntitySuper implements JsonSerializable {
115115

116116
public const META_ID_TITLE = 'id';
117117

118+
public const RELATION_TYPE = 'relationType';
119+
public const RELATION_TARGET_ID = 'targetId';
120+
public const RELATION_LABEL_COLUMN = 'labelColumn';
121+
118122
protected ?string $title = null;
119123
protected ?int $tableId = null;
120124
protected ?string $createdBy = null;

lib/Service/ColumnTypes/RelationBusiness.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace OCA\Tables\Service\ColumnTypes;
99

1010
use OCA\Tables\Db\Column;
11+
use OCA\Tables\Errors\BadRequestError;
1112
use OCA\Tables\Service\RelationService;
1213
use Psr\Log\LoggerInterface;
1314

@@ -73,4 +74,25 @@ public function canBeParsed($value, ?Column $column = null): bool {
7374

7475
return false;
7576
}
77+
78+
public function validateValue(mixed $value, Column $column, string $userId, int $tableId, ?int $rowId): void {
79+
if ($value === null || $value === '') {
80+
return;
81+
}
82+
// Validate that the value exists in the target table/view
83+
$relationData = $this->relationService->getRelationData($column);
84+
85+
// Try to find value by label first
86+
$matchingRelation = array_filter($relationData, fn (array $relation) => $relation['label'] === $value);
87+
if (!empty($matchingRelation)) {
88+
return;
89+
}
90+
91+
// If not found by label, try to find by id
92+
if (is_numeric($value) && isset($relationData[(int)$value])) {
93+
return;
94+
}
95+
96+
throw new BadRequestError('Relation value does not exist in the target table/view');
97+
}
7698
}

lib/Service/RelationService.php

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,34 +15,18 @@
1515
use OCA\Tables\Errors\NotFoundError;
1616
use OCA\Tables\Errors\PermissionError;
1717
use OCP\AppFramework\Db\DoesNotExistException;
18-
use Psr\Log\LoggerInterface;
1918

2019
class RelationService {
21-
22-
private ColumnMapper $columnMapper;
23-
private ViewMapper $viewMapper;
24-
private Row2Mapper $row2Mapper;
25-
private ColumnService $columnService;
26-
private LoggerInterface $logger;
27-
private ?string $userId;
28-
2920
/** @var array<string, array> Cache for relation data */
3021
private array $cacheRelationData = [];
3122

3223
public function __construct(
33-
ColumnMapper $columnMapper,
34-
ViewMapper $viewMapper,
35-
Row2Mapper $row2Mapper,
36-
ColumnService $columnService,
37-
LoggerInterface $logger,
38-
?string $userId,
24+
private ColumnMapper $columnMapper,
25+
private ViewMapper $viewMapper,
26+
private Row2Mapper $row2Mapper,
27+
private ColumnService $columnService,
28+
private ?string $userId,
3929
) {
40-
$this->columnMapper = $columnMapper;
41-
$this->viewMapper = $viewMapper;
42-
$this->row2Mapper = $row2Mapper;
43-
$this->columnService = $columnService;
44-
$this->logger = $logger;
45-
$this->userId = $userId;
4630
}
4731

4832
/**
@@ -171,16 +155,16 @@ private function getRelationDataForTarget(string $target, Column $column): array
171155
}
172156

173157
$settings = $column->getCustomSettingsArray();
174-
if (empty($settings['relationType']) || empty($settings['targetId']) || empty($settings['labelColumn'])) {
158+
if (empty($settings[Column::RELATION_TYPE]) || empty($settings[Column::RELATION_TARGET_ID]) || empty($settings[Column::RELATION_LABEL_COLUMN])) {
175159
$this->cacheRelationData[$cacheKey] = [];
176160
return [];
177161
}
178162

179-
$isView = $settings['relationType'] === 'view';
180-
$targetId = $settings['targetId'] ?? null;
163+
$isView = $settings[Column::RELATION_TYPE] === 'view';
164+
$targetId = $settings[Column::RELATION_TARGET_ID] ?? null;
181165

182166
try {
183-
$targetColumn = $this->columnMapper->find($settings['labelColumn']);
167+
$targetColumn = $this->columnMapper->find($settings[Column::RELATION_LABEL_COLUMN]);
184168
if ($isView) {
185169
$view = $this->viewMapper->find($targetId);
186170
$rows = $this->row2Mapper->findAll(
@@ -212,7 +196,7 @@ private function getRelationDataForTarget(string $target, Column $column): array
212196
foreach ($rows as $row) {
213197
$data = $row->getData();
214198
$displayFieldData = array_filter($data, function ($item) use ($settings) {
215-
return $item['columnId'] === (int)$settings['labelColumn'];
199+
return $item['columnId'] === (int)$settings[Column::RELATION_LABEL_COLUMN];
216200
});
217201
$value = reset($displayFieldData)['value'] ?? null;
218202

0 commit comments

Comments
 (0)