mirror of
https://github.com/nextcloud/tables.git
synced 2026-01-17 13:31:56 +00:00
831 lines
28 KiB
PHP
831 lines
28 KiB
PHP
<?php
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace OCA\Tables\Service;
|
|
|
|
use OCA\Tables\Activity\ActivityManager;
|
|
use OCA\Tables\Db\Column;
|
|
use OCA\Tables\Db\ColumnMapper;
|
|
use OCA\Tables\Db\Row2;
|
|
use OCA\Tables\Db\Row2Mapper;
|
|
use OCA\Tables\Db\TableMapper;
|
|
use OCA\Tables\Db\View;
|
|
use OCA\Tables\Db\ViewMapper;
|
|
use OCA\Tables\Errors\BadRequestError;
|
|
use OCA\Tables\Errors\InternalError;
|
|
use OCA\Tables\Errors\NotFoundError;
|
|
use OCA\Tables\Errors\PermissionError;
|
|
use OCA\Tables\Event\RowAddedEvent;
|
|
use OCA\Tables\Event\RowDeletedEvent;
|
|
use OCA\Tables\Event\RowUpdatedEvent;
|
|
use OCA\Tables\Helper\ColumnsHelper;
|
|
use OCA\Tables\Model\RowDataInput;
|
|
use OCA\Tables\ResponseDefinitions;
|
|
use OCA\Tables\Service\ColumnTypes\IColumnTypeBusiness;
|
|
use OCA\Tables\Service\ValueObject\ViewColumnInformation;
|
|
use OCP\AppFramework\Db\DoesNotExistException;
|
|
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
|
use OCP\DB\Exception;
|
|
use OCP\EventDispatcher\IEventDispatcher;
|
|
use OCP\Server;
|
|
use Psr\Container\ContainerExceptionInterface;
|
|
use Psr\Container\NotFoundExceptionInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
/**
|
|
* @psalm-import-type TablesRow from ResponseDefinitions
|
|
*/
|
|
class RowService extends SuperService {
|
|
private array $tmpRows = []; // holds already loaded rows as a small cache
|
|
|
|
public function __construct(
|
|
LoggerInterface $logger,
|
|
?string $userId,
|
|
PermissionsService $permissionsService,
|
|
private ColumnMapper $columnMapper,
|
|
private ViewMapper $viewMapper,
|
|
private TableMapper $tableMapper,
|
|
private Row2Mapper $row2Mapper,
|
|
private IEventDispatcher $eventDispatcher,
|
|
private ColumnsHelper $columnsHelper,
|
|
private ActivityManager $activityManager,
|
|
) {
|
|
parent::__construct($logger, $userId, $permissionsService);
|
|
|
|
}
|
|
|
|
/**
|
|
* @param Row2[] $rows
|
|
* @psalm-return TablesRow[]
|
|
*/
|
|
public function formatRows(array $rows): array {
|
|
return array_map(fn (Row2 $row) => $row->jsonSerialize(), $rows);
|
|
}
|
|
|
|
/**
|
|
* @param int $tableId
|
|
* @param string $userId
|
|
* @param ?int $limit
|
|
* @param ?int $offset
|
|
* @return Row2[]
|
|
* @throws InternalError
|
|
* @throws PermissionError
|
|
*/
|
|
public function findAllByTable(int $tableId, string $userId, ?int $limit = null, ?int $offset = null): array {
|
|
try {
|
|
if ($this->permissionsService->canReadRowsByElementId($tableId, 'table', $userId)) {
|
|
$tableColumns = $this->columnMapper->findAllByTable($tableId);
|
|
$showColumnIds = array_map(fn (Column $column) => $column->getId(), $tableColumns);
|
|
|
|
return $this->row2Mapper->findAll($showColumnIds, $tableId, $limit, $offset, null, null, $userId);
|
|
} else {
|
|
throw new PermissionError('no read access to table id = ' . $tableId);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logger->error($e->getMessage());
|
|
throw new InternalError($e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $viewId
|
|
* @param string $userId
|
|
* @param int|null $limit
|
|
* @param int|null $offset
|
|
* @return Row2[]
|
|
* @throws DoesNotExistException
|
|
* @throws InternalError
|
|
* @throws MultipleObjectsReturnedException
|
|
* @throws PermissionError
|
|
*/
|
|
public function findAllByView(int $viewId, string $userId, ?int $limit = null, ?int $offset = null): array {
|
|
try {
|
|
if ($this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) {
|
|
$view = $this->viewMapper->find($viewId);
|
|
|
|
return $this->row2Mapper->findAll($view->getColumnIds(), $view->getTableId(), $limit, $offset, $view->getFilterArray(), $view->getSortArray(), $userId);
|
|
} else {
|
|
throw new PermissionError('no read access to view id = ' . $viewId);
|
|
}
|
|
} catch (Exception $e) {
|
|
$this->logger->error($e->getMessage());
|
|
throw new InternalError($e->getMessage());
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @param int $rowId
|
|
* @return Row2
|
|
* @throws InternalError
|
|
* @throws NotFoundError
|
|
* @throws PermissionError
|
|
*/
|
|
public function find(int $rowId): Row2 {
|
|
try {
|
|
$tableId = $this->row2Mapper->getTableIdForRow($rowId);
|
|
|
|
if (!$this->permissionsService->canReadRowsByElementId($tableId, 'table')) {
|
|
throw new PermissionError('PermissionError: can not read row with id ' . $rowId);
|
|
}
|
|
|
|
$columns = $this->columnMapper->findAllByTable($tableId);
|
|
$row = $this->row2Mapper->find($rowId, $columns);
|
|
} catch (Exception|MultipleObjectsReturnedException $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
} catch (NotFoundError|DoesNotExistException $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* @param int|null $tableId
|
|
* @param int|null $viewId
|
|
* @param RowDataInput|list<array{columnId: int, value: mixed}> $data
|
|
* @return Row2
|
|
*
|
|
* @throws BadRequestError
|
|
* @throws NotFoundError
|
|
* @throws PermissionError
|
|
* @throws Exception
|
|
* @throws InternalError
|
|
*/
|
|
public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): Row2 {
|
|
if ($this->userId === null || $this->userId === '') {
|
|
$e = new \Exception('No user id in context, but needed.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
$view = null;
|
|
|
|
if ($viewId) {
|
|
try {
|
|
$view = $this->viewMapper->find($viewId);
|
|
} catch (DoesNotExistException $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError('Given view could not be found. More details can be found in the log.');
|
|
} catch (InternalError|Exception|MultipleObjectsReturnedException $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
// security
|
|
if (!$this->permissionsService->canCreateRows($view)) {
|
|
throw new PermissionError('create row at the view id = ' . $viewId . ' is not allowed.');
|
|
}
|
|
|
|
$columns = $this->columnMapper->findAll($view->getColumnIds());
|
|
}
|
|
if ($tableId) {
|
|
try {
|
|
$table = $this->tableMapper->find($tableId);
|
|
} catch (DoesNotExistException $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError('Given table could not be found. More details can be found in the log.');
|
|
} catch (MultipleObjectsReturnedException|Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
// security
|
|
if (!$this->permissionsService->canCreateRows($table, 'table')) {
|
|
throw new PermissionError('create row at the table id = ' . $tableId . ' is not allowed.');
|
|
}
|
|
|
|
$columns = $this->columnMapper->findAllByTable($tableId);
|
|
}
|
|
|
|
if (!$viewId && !$tableId) {
|
|
throw new InternalError('Cannot create row without table or view in context');
|
|
}
|
|
|
|
$tableId = $tableId ?? $view->getTableId();
|
|
|
|
$data = $data instanceof RowDataInput ? $data : RowDataInput::fromArray($data);
|
|
$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);
|
|
try {
|
|
$insertedRow = $this->row2Mapper->insert($row2);
|
|
|
|
$this->eventDispatcher->dispatchTyped(new RowAddedEvent($insertedRow));
|
|
$this->activityManager->triggerEvent(
|
|
objectType: ActivityManager::TABLES_OBJECT_ROW,
|
|
object: $insertedRow,
|
|
subject: ActivityManager::SUBJECT_ROW_CREATE,
|
|
author: $this->userId,
|
|
);
|
|
|
|
return $this->filterRowResult($view, $insertedRow);
|
|
} catch (InternalError|Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When inserting rows into views we try to prefill columns that are not accessible by reasonable defaults
|
|
*
|
|
* This might not work in all cases, but for single filter rules this is the sanest to ensure the row is actually part of the view
|
|
*/
|
|
private function enhanceWithViewDefaults(?View $view, RowDataInput $data): RowDataInput {
|
|
if ($view === null) {
|
|
return $data;
|
|
}
|
|
|
|
$filters = $view->getFilterArray();
|
|
if (empty($filters)) {
|
|
return $data;
|
|
}
|
|
|
|
// Process each filter rule group (AND groups)
|
|
foreach ($filters as $filterRules) {
|
|
if (!is_array($filterRules)) {
|
|
continue;
|
|
}
|
|
|
|
// Process each filter within the group (OR conditions)
|
|
foreach ($filterRules as $filter) {
|
|
if (!is_array($filter) || !isset($filter['columnId'], $filter['operator'], $filter['value'])) {
|
|
continue;
|
|
}
|
|
|
|
// Skip if the column is already visible in the view
|
|
if (in_array($filter['columnId'], $view->getColumnIds())) {
|
|
continue;
|
|
}
|
|
|
|
// For meta columns, we don't need to add them to the data since they are handled separately
|
|
if (Column::isValidMetaTypeId($filter['columnId'])) {
|
|
continue;
|
|
}
|
|
|
|
// Only handle simple equality filters for now
|
|
if (!in_array($filter['operator'], ['is-equal', 'is-not-equal'])) {
|
|
continue;
|
|
}
|
|
|
|
// Only set the default if the column hasn't been set yet
|
|
if (!$data->hasColumn($filter['columnId'])) {
|
|
$data->add($filter['columnId'], $this->columnsHelper->resolveSearchValue((string)$filter['value'], $this->userId));
|
|
}
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, true>
|
|
*/
|
|
private function extractColumnsByProperty(View $view, string $property): array {
|
|
return array_reduce(
|
|
$view->getColumnsSettingsArray(),
|
|
static function (array $carry, ViewColumnInformation $column) use ($property) {
|
|
if (method_exists($column, $property) && $column->{$property}()) {
|
|
$carry[$column->getId()] = true;
|
|
}
|
|
return $carry;
|
|
},
|
|
[]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param RowDataInput $data
|
|
* @throws InternalError
|
|
* @throws BadRequestError
|
|
* @return RowDataInput
|
|
*/
|
|
private function cleanupAndValidateData(RowDataInput $data, array $columns, ?int $tableId, ?int $viewId, ?int $rowId = null): RowDataInput {
|
|
$view = $viewId ? $this->viewMapper->find($viewId) : null;
|
|
$readOnlyColumns = $view ? $this->extractColumnsByProperty($view, 'isReadonly') : [];
|
|
$mandatoryColumns = $view ? $this->extractColumnsByProperty($view, 'isMandatory') : [];
|
|
|
|
$out = new RowDataInput();
|
|
foreach ($data as $entry) {
|
|
$columnId = (int)$entry['columnId'];
|
|
|
|
// Skip metadata columns
|
|
if (Column::isValidMetaTypeId($columnId)) {
|
|
continue;
|
|
}
|
|
|
|
$column = $this->getColumnFromColumnsArray($columnId, $columns);
|
|
|
|
if ($column) {
|
|
$columnBusiness = $this->getColumnBusiness($column);
|
|
$columnBusiness->validateValue($entry['value'], $column, $this->userId, $tableId, $rowId);
|
|
}
|
|
|
|
// check if it is allowed to insert a value for the requested column
|
|
if (!$column && $viewId) {
|
|
throw new InternalError('Column with id ' . $entry['columnId'] . ' is not part of view with id ' . $viewId);
|
|
} elseif (!$column && $tableId) {
|
|
throw new InternalError('Column with id ' . $entry['columnId'] . ' is not part of table with id ' . $tableId);
|
|
}
|
|
|
|
if (!$column) {
|
|
$e = new \Exception('No column found, can not parse value.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
if (!empty($readOnlyColumns[$entry['columnId']])) {
|
|
continue;
|
|
}
|
|
|
|
// parse given value to respect the column type value format
|
|
$out->add((int)$entry['columnId'], $this->parseValueByColumnType($column, $entry['value']));
|
|
}
|
|
|
|
if ($viewId && !empty($mandatoryColumns)) {
|
|
$existingRow = null;
|
|
if ($rowId !== null) {
|
|
try {
|
|
$existingRow = $this->getRowById($rowId);
|
|
} catch (NotFoundError|InternalError $e) {
|
|
$this->logger->debug('Could not load existing row for mandatory validation', ['rowId' => $rowId, 'exception' => $e]);
|
|
}
|
|
}
|
|
$this->validateMandatoryColumns($mandatoryColumns, $out, $columns, $existingRow);
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, bool> $mandatoryColumns
|
|
* @param RowDataInput $data
|
|
* @param Column[] $columns
|
|
* @param Row2|null $existingRow
|
|
* @throws BadRequestError
|
|
* @throws InternalError
|
|
*/
|
|
private function validateMandatoryColumns(array $mandatoryColumns, RowDataInput $data, array $columns, ?Row2 $existingRow = null): void {
|
|
foreach ($mandatoryColumns as $columnId => $isMandatory) {
|
|
if (!$isMandatory) {
|
|
continue;
|
|
}
|
|
|
|
$column = $this->getColumnFromColumnsArray($columnId, $columns);
|
|
if (!$column) {
|
|
continue;
|
|
}
|
|
|
|
$hasValue = false;
|
|
$value = null;
|
|
|
|
foreach ($data as $entry) {
|
|
if ($entry['columnId'] === $columnId) {
|
|
$value = $entry['value'];
|
|
$hasValue = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$hasValue && $existingRow !== null) {
|
|
foreach ($existingRow->getData() as $existingEntry) {
|
|
if ($existingEntry['columnId'] === $columnId) {
|
|
$value = $existingEntry['value'];
|
|
$hasValue = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($hasValue) {
|
|
try {
|
|
$columnBusiness = $this->getColumnBusiness($column);
|
|
$isValid = $this->isValueValidForMandatoryColumn($value, $column, $columnBusiness);
|
|
if (!$isValid) {
|
|
throw new BadRequestError(
|
|
'Mandatory column "' . $column->getTitle() . '" cannot be empty or invalid.'
|
|
);
|
|
}
|
|
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
|
|
$this->logger->debug('Column type business class not found for mandatory validation', ['exception' => $e]);
|
|
}
|
|
} else {
|
|
throw new BadRequestError(
|
|
'Mandatory column "' . $column->getTitle() . '" cannot be empty.'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function isValueValidForMandatoryColumn($value, Column $column, IColumnTypeBusiness $columnBusiness): bool {
|
|
if ($column->getSubtype() === Column::SUBTYPE_SELECTION_CHECK) {
|
|
return true;
|
|
}
|
|
|
|
if ($value === null || $value === '') {
|
|
return false;
|
|
}
|
|
if ($column->getType() === Column::TYPE_SELECTION) {
|
|
if (is_array($value)) {
|
|
return count($value) > 0;
|
|
}
|
|
if (is_numeric($value) && $value > 0) {
|
|
return true;
|
|
}
|
|
return $column->getSelectionDefault() !== null && $column->getSelectionDefault() !== '';
|
|
}
|
|
if ($column->getSubtype() === Column::SUBTYPE_SELECTION_MULTI) {
|
|
if (is_array($value)) {
|
|
return count($value) > 0;
|
|
}
|
|
$defaultValue = $column->getSelectionDefault();
|
|
return $defaultValue !== null && $defaultValue !== '' && $defaultValue !== '[]';
|
|
}
|
|
return $value !== null && $value !== '' && $value !== [];
|
|
}
|
|
|
|
/**
|
|
* @param Column $column
|
|
* @param string|array|int|float|bool|null $value
|
|
* @return array|string|int|float|null
|
|
*/
|
|
private function parseValueByColumnType(Column $column, $value = null) {
|
|
try {
|
|
$columnBusiness = $this->getColumnBusiness($column);
|
|
if ($columnBusiness->canBeParsed($value, $column)) {
|
|
return json_decode($columnBusiness->parseValue($value, $column), true);
|
|
}
|
|
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
|
|
$this->logger->debug('Column type business class not found', ['exception' => $e]);
|
|
}
|
|
$this->logger->warning('Value could not be parsed for column ' . $column->getTitle(), [$value]);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param int $columnId
|
|
* @param Column[] $columns
|
|
* @return Column|null
|
|
*/
|
|
private function getColumnFromColumnsArray(int $columnId, array $columns): ?Column {
|
|
foreach ($columns as $column) {
|
|
if ($column->getId() === $columnId) {
|
|
return $column;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @throws NotFoundError
|
|
* @throws InternalError
|
|
*/
|
|
private function getRowById(int $rowId): Row2 {
|
|
if (isset($this->tmpRows[$rowId])) {
|
|
return $this->tmpRows[$rowId];
|
|
}
|
|
|
|
try {
|
|
if ($this->row2Mapper->getTableIdForRow($rowId) === null) {
|
|
$e = new \Exception('No table id in row, but needed.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
$row = $this->row2Mapper->find($rowId, $this->columnMapper->findAllByTable($this->row2Mapper->getTableIdForRow($rowId)));
|
|
$row->markAsLoaded();
|
|
} catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
} catch (NotFoundError $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
$this->tmpRows[$rowId] = $row;
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Update multiple cells in a row
|
|
*
|
|
* @param int $id
|
|
* @param int|null $viewId
|
|
* @param list<array{columnId: string|int, value: mixed}> $data
|
|
* @param string $userId
|
|
* @return Row2
|
|
*
|
|
* @throws BadRequestError
|
|
* @throws InternalError
|
|
* @throws NotFoundError
|
|
* @throws PermissionError
|
|
* @noinspection DuplicatedCode
|
|
*/
|
|
public function updateSet(
|
|
int $id,
|
|
?int $viewId,
|
|
array $data,
|
|
string $userId,
|
|
?int $tableId,
|
|
): Row2 {
|
|
try {
|
|
$item = $this->getRowById($id);
|
|
} catch (InternalError $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
} catch (NotFoundError $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
if ($viewId) {
|
|
// security
|
|
if (!$this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) {
|
|
$e = new \Exception('Row not found.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
if (!$this->permissionsService->canUpdateRowsByViewId($viewId)) {
|
|
$e = new \Exception('Update row is not allowed.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
try {
|
|
$view = $this->viewMapper->find($viewId);
|
|
} catch (InternalError|MultipleObjectsReturnedException|Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
} catch (DoesNotExistException $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
// is row in view?
|
|
if (!$this->row2Mapper->isRowInViewPresent($id, $view, $userId)) {
|
|
$e = new \Exception('Update row is not allowed.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
// fetch all needed columns
|
|
try {
|
|
$columns = $this->columnMapper->findAll($view->getColumnIds());
|
|
} catch (Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
} else {
|
|
if ($tableId === null) {
|
|
$tableId = $item->getTableId();
|
|
}
|
|
if ($tableId !== $item->getTableId()) {
|
|
$e = new \Exception('Row does not belong to table with id ' . $tableId);
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
// security
|
|
if (!$this->permissionsService->canReadRowsByElementId($item->getTableId(), 'table', $userId)) {
|
|
$e = new \Exception('Row not found.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
if (!$this->permissionsService->canUpdateRowsByTableId($tableId)) {
|
|
$e = new \Exception('Update row is not allowed.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
try {
|
|
$columns = $this->columnMapper->findAllByTable($tableId);
|
|
} catch (Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
$previousData = $item->getData();
|
|
$data = RowDataInput::fromArray($data);
|
|
$data = $this->cleanupAndValidateData($data, $columns, $item->getTableId(), $viewId, $id);
|
|
|
|
foreach ($data as $entry) {
|
|
// Check whether the column of which the value should change is part of the table / view
|
|
$column = $this->getColumnFromColumnsArray($entry['columnId'], $columns);
|
|
if ($column) {
|
|
$item->insertOrUpdateCell($entry);
|
|
} else {
|
|
$this->logger->warning('Column to update row not found, will continue and ignore this.');
|
|
}
|
|
}
|
|
|
|
$updatedRow = $this->row2Mapper->update($item);
|
|
|
|
$this->eventDispatcher->dispatchTyped(new RowUpdatedEvent($updatedRow, $previousData));
|
|
|
|
if ($updatedRow->getData() !== $previousData) {
|
|
$this->activityManager->triggerEvent(
|
|
objectType: ActivityManager::TABLES_OBJECT_ROW,
|
|
object: $updatedRow,
|
|
subject: ActivityManager::SUBJECT_ROW_UPDATE,
|
|
author: $this->userId,
|
|
additionalParams: [
|
|
'before' => $previousData,
|
|
'after' => $updatedRow->getData(),
|
|
]
|
|
);
|
|
}
|
|
|
|
return $this->filterRowResult($view ?? null, $updatedRow);
|
|
}
|
|
|
|
/**
|
|
* @param int $id
|
|
* @param int|null $viewId
|
|
* @param string $userId
|
|
* @return Row2
|
|
*
|
|
* @throws InternalError
|
|
* @throws NotFoundError
|
|
* @throws PermissionError
|
|
* @noinspection DuplicatedCode
|
|
*/
|
|
public function delete(int $id, ?int $viewId, string $userId): Row2 {
|
|
try {
|
|
$item = $this->getRowById($id);
|
|
} catch (InternalError $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
} catch (NotFoundError $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
if ($viewId) {
|
|
// security
|
|
if (!$this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) {
|
|
$e = new \Exception('Row not found.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
if (!$this->permissionsService->canDeleteRowsByViewId($viewId)) {
|
|
$e = new \Exception('Update row is not allowed.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
try {
|
|
$view = $this->viewMapper->find($viewId);
|
|
} catch (InternalError|DoesNotExistException|MultipleObjectsReturnedException|Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
if (!$this->row2Mapper->isRowInViewPresent($id, $view, $userId)) {
|
|
$e = new \Exception('Update row is not allowed.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
} else {
|
|
// security
|
|
if (!$this->permissionsService->canReadRowsByElementId($item->getTableId(), 'table', $userId)) {
|
|
$e = new \Exception('Row not found.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
if (!$this->permissionsService->canDeleteRowsByTableId($item->getTableId())) {
|
|
$e = new \Exception('Update row is not allowed.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
try {
|
|
$deletedRow = $this->row2Mapper->delete($item);
|
|
|
|
$event = new RowDeletedEvent($item, $item->getData());
|
|
|
|
$this->eventDispatcher->dispatchTyped($event);
|
|
$this->activityManager->triggerEvent(
|
|
objectType: ActivityManager::TABLES_OBJECT_ROW,
|
|
object: $deletedRow,
|
|
subject: ActivityManager::SUBJECT_ROW_DELETE,
|
|
author: $this->userId,
|
|
);
|
|
|
|
return $this->filterRowResult($view ?? null, $deletedRow);
|
|
} catch (Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $tableId
|
|
* @param null|string $userId
|
|
*
|
|
* @throws PermissionError
|
|
* @throws Exception
|
|
*/
|
|
public function deleteAllByTable(int $tableId, ?string $userId = null): void {
|
|
// security
|
|
if (!$this->permissionsService->canDeleteRowsByTableId($tableId, $userId)) {
|
|
throw new PermissionError('delete all rows for table id = ' . $tableId . ' is not allowed.');
|
|
}
|
|
|
|
$columns = $this->columnMapper->findAllByTable($tableId);
|
|
|
|
$this->row2Mapper->deleteAllForTable($tableId, $columns);
|
|
}
|
|
|
|
/**
|
|
* This deletes all data for a column, eg if the columns gets removed
|
|
*
|
|
* >>> SECURITY <<<
|
|
* We do not check if you are allowed to remove this data. That has to be done before!
|
|
* Why? Mostly this check will have be run before and we can pass this here due to performance reasons.
|
|
*
|
|
* @param Column $column
|
|
* @throws InternalError
|
|
*/
|
|
public function deleteColumnDataFromRows(Column $column):void {
|
|
$this->row2Mapper->deleteDataForColumn($column);
|
|
}
|
|
|
|
/**
|
|
* @param int $tableId
|
|
* @return int
|
|
*
|
|
* @throws PermissionError
|
|
*/
|
|
public function getRowsCount(int $tableId): int {
|
|
if ($this->permissionsService->canReadRowsByElementId($tableId, 'table')) {
|
|
return $this->row2Mapper->countRowsForTable($tableId);
|
|
} else {
|
|
throw new PermissionError('no read access for counting to table id = ' . $tableId);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param View $view
|
|
* @param string $userId
|
|
* @return int
|
|
*
|
|
* @throws PermissionError
|
|
*/
|
|
public function getViewRowsCount(View $view, string $userId): int {
|
|
if ($this->permissionsService->canReadRowsByElementId($view->getId(), 'view', $userId)) {
|
|
try {
|
|
return $this->row2Mapper->countRowsForView($view, $userId);
|
|
} catch (Exception $e) {
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
return 0;
|
|
}
|
|
} else {
|
|
throw new PermissionError('no read access for counting to view id = ' . $view->getId());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $rowId
|
|
* @param int $viewId
|
|
* @param string $userId
|
|
* @return bool
|
|
*
|
|
* @throws PermissionError
|
|
*/
|
|
public function isRowInViewPresent(int $rowId, int $viewId, string $userId): bool {
|
|
if (!$this->permissionsService->canReadRowsByElementId($viewId, 'view', $userId)) {
|
|
$e = new \Exception('Row not found.');
|
|
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
|
throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
|
|
}
|
|
|
|
$view = $this->viewMapper->find($viewId);
|
|
return $this->row2Mapper->isRowInViewPresent($rowId, $view, $userId);
|
|
}
|
|
|
|
private function filterRowResult(?View $view, Row2 $row): Row2 {
|
|
if ($view === null) {
|
|
return $row;
|
|
}
|
|
|
|
$row->filterDataByColumns($view->getColumnIds());
|
|
|
|
return $row;
|
|
}
|
|
|
|
private function getColumnBusiness(Column $column): IColumnTypeBusiness {
|
|
$businessClassName = 'OCA\Tables\Service\ColumnTypes\\';
|
|
$businessClassName .= ucfirst($column->getType()) . ucfirst($column->getSubtype()) . 'Business';
|
|
/** @var IColumnTypeBusiness $columnBusiness */
|
|
$columnBusiness = Server::get($businessClassName);
|
|
|
|
return $columnBusiness;
|
|
}
|
|
}
|