Skip to content

Commit 502b69b

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

File tree

3 files changed

+41
-5
lines changed

3 files changed

+41
-5
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: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class RelationService {
2323
private ViewMapper $viewMapper;
2424
private Row2Mapper $row2Mapper;
2525
private ColumnService $columnService;
26+
private PermissionsService $permissionsService;
2627
private LoggerInterface $logger;
2728
private ?string $userId;
2829

@@ -34,13 +35,15 @@ public function __construct(
3435
ViewMapper $viewMapper,
3536
Row2Mapper $row2Mapper,
3637
ColumnService $columnService,
38+
PermissionsService $permissionsService,
3739
LoggerInterface $logger,
3840
?string $userId,
3941
) {
4042
$this->columnMapper = $columnMapper;
4143
$this->viewMapper = $viewMapper;
4244
$this->row2Mapper = $row2Mapper;
4345
$this->columnService = $columnService;
46+
$this->permissionsService = $permissionsService;
4447
$this->logger = $logger;
4548
$this->userId = $userId;
4649
}
@@ -171,16 +174,23 @@ private function getRelationDataForTarget(string $target, Column $column): array
171174
}
172175

173176
$settings = $column->getCustomSettingsArray();
174-
if (empty($settings['relationType']) || empty($settings['targetId']) || empty($settings['labelColumn'])) {
177+
if (empty($settings[Column::RELATION_TYPE]) || empty($settings[Column::RELATION_TARGET_ID]) || empty($settings[Column::RELATION_LABEL_COLUMN])) {
175178
$this->cacheRelationData[$cacheKey] = [];
176179
return [];
177180
}
178181

179-
$isView = $settings['relationType'] === 'view';
180-
$targetId = $settings['targetId'] ?? null;
182+
if (!$this->permissionsService->canReadRowsByElementId($settings[Column::RELATION_TARGET_ID], $settings[Column::RELATION_TYPE], $this->userId)) {
183+
$e = new \Exception('Row not found.');
184+
$this->logger->error($e->getMessage(), ['exception' => $e]);
185+
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
186+
}
187+
188+
189+
$isView = $settings[Column::RELATION_TYPE] === 'view';
190+
$targetId = $settings[Column::RELATION_TARGET_ID] ?? null;
181191

182192
try {
183-
$targetColumn = $this->columnMapper->find($settings['labelColumn']);
193+
$targetColumn = $this->columnMapper->find($settings[Column::RELATION_LABEL_COLUMN]);
184194
if ($isView) {
185195
$view = $this->viewMapper->find($targetId);
186196
$rows = $this->row2Mapper->findAll(
@@ -212,7 +222,7 @@ private function getRelationDataForTarget(string $target, Column $column): array
212222
foreach ($rows as $row) {
213223
$data = $row->getData();
214224
$displayFieldData = array_filter($data, function ($item) use ($settings) {
215-
return $item['columnId'] === (int)$settings['labelColumn'];
225+
return $item['columnId'] === (int)$settings[Column::RELATION_LABEL_COLUMN];
216226
});
217227
$value = reset($displayFieldData)['value'] ?? null;
218228

0 commit comments

Comments
 (0)