Files
cms-form/Classes/Storage/DatabaseStorageAdapter.php

353 lines
11 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Form\Storage;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Form\Domain\DTO\FormData;
use TYPO3\CMS\Form\Domain\DTO\FormMetadata;
use TYPO3\CMS\Form\Domain\DTO\SearchCriteria;
use TYPO3\CMS\Form\Domain\DTO\StorageContext;
use TYPO3\CMS\Form\Domain\Repository\FormDefinitionRepository;
use TYPO3\CMS\Form\Domain\ValueObject\FormIdentifier;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
use TYPO3\CMS\Form\Storage\Permission\DatabasePermissionChecker;
/**
* Storage adapter for database-based form persistence
*
* Scope: frontend / backend
* @internal
*/
final readonly class DatabaseStorageAdapter implements StorageAdapterInterface
{
public function __construct(
private FormDefinitionRepository $repository,
private DatabasePermissionChecker $permissionChecker,
private JsonObjectKeyOrderPreserver $jsonObjectKeyOrderPreserver,
) {}
public function getTypeIdentifier(): string
{
return 'database';
}
public function supports(string $identifier): bool
{
return str_starts_with($identifier, 'NEW') || MathUtility::canBeInterpretedAsInteger($identifier);
}
public function getPriority(): int
{
return 100;
}
public function getLabel(): string
{
return 'formManager.storage.database.label';
}
public function getDescription(): string
{
return 'formManager.storage.database.description';
}
public function getIconIdentifier(): string
{
return 'content-database';
}
public function getUniquePersistenceIdentifier(string $formIdentifier, string $storageLocation): string
{
return StringUtility::getUniqueId('NEW');
}
/**
* @throws PersistenceManagerException
*/
public function read(FormIdentifier $identifier, ?ServerRequestInterface $request = null): FormData
{
$uid = $this->extractUidFromIdentifier($identifier);
$record = $this->repository->findByUid($uid);
if (!$record) {
throw new PersistenceManagerException(
sprintf('The form with uid "%s" could not be loaded.', $uid),
1767199422
);
}
$applicationType = $request !== null ? ApplicationType::fromRequest($request) : null;
// Skip permission checks in frontend context: Forms must be readable without a
// backend user session, so no backend permission checks are applied for frontend
// requests. In all other contexts (e.g. backend), permission checks are enforced.
if (!$applicationType?->isFrontend()) {
$this->permissionChecker->assertReadAccessForRecord($uid, $record);
}
try {
$formDefinitionArray = json_decode($record['configuration'] ?? '', true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new PersistenceManagerException(
sprintf('The form definition for uid "%s" is invalid: %s', $uid, $e->getMessage()),
1767199423,
$e
);
}
if (!is_array($formDefinitionArray)) {
throw new PersistenceManagerException(
sprintf('The form definition for uid "%s" is invalid.', $uid),
1767199444
);
}
$formDefinitionArray = $this->jsonObjectKeyOrderPreserver->restore($formDefinitionArray);
$formDefinitionArray['identifier'] = $record['identifier'];
return FormData::fromArray($formDefinitionArray);
}
/**
* @throws PersistenceManagerException
*/
public function write(FormIdentifier $identifier, FormData $data, ?StorageContext $context = null): FormIdentifier
{
if (!$this->exists($identifier)) {
$pid = 0;
if (!$this->permissionChecker->hasWritePermission($pid)) {
throw new PersistenceManagerException(
'Access denied: You do not have permission to create a form.',
1767199435
);
}
$uid = $this->repository->add($identifier->identifier, $pid, $data);
if (!$uid) {
throw new PersistenceManagerException(
'Failed to create form definition in database.',
1767199424
);
}
return new FormIdentifier((string)$uid);
}
$uid = $this->extractUidFromIdentifier($identifier);
$record = $this->repository->findByUid($uid);
if (!$record) {
throw new PersistenceManagerException(
sprintf('The form with uid "%s" could not be found.', $uid),
1767199425
);
}
$this->permissionChecker->assertWriteAccessForRecord($uid, $record);
$result = $this->repository->update($uid, $data);
if (!$result) {
throw new PersistenceManagerException(
sprintf('Failed to update form definition with uid "%s".', $uid),
1767199426
);
}
return $identifier;
}
/**
* @throws PersistenceManagerException
*/
public function delete(FormIdentifier $identifier): void
{
$uid = $this->extractUidFromIdentifier($identifier);
$record = $this->repository->findByUid($uid);
if (!$record) {
throw new PersistenceManagerException(
sprintf('The form with uid "%s" could not be found.', $uid),
1767199431
);
}
$this->permissionChecker->assertWriteAccessForRecord($uid, $record);
$success = $this->repository->remove($uid);
if (!$success) {
throw new PersistenceManagerException(
sprintf('Failed to delete form definition with uid "%s".', $uid),
1767199427
);
}
}
/**
* @throws PersistenceManagerException
*/
public function exists(FormIdentifier $identifier): bool
{
if (str_starts_with($identifier->identifier, 'NEW')) {
return false;
}
$uid = $this->extractUidFromIdentifier($identifier);
$record = $this->repository->findByUid($uid);
if ($record === null) {
return false;
}
$pid = (int)($record['pid'] ?? -1);
return $this->permissionChecker->hasReadPermission($pid);
}
public function existsByFormIdentifier(string $formIdentifier): bool
{
return $this->repository->existsByFormIdentifier($formIdentifier);
}
/**
* Find all form definitions for listing.
*
* Uses findAllForListing() which only selects metadata columns (uid, pid, identifier, label)
* instead of the full configuration JSON. This avoids loading and parsing potentially large
* JSON blobs just for the form listing view.
*/
public function findAll(SearchCriteria $criteria): array
{
$rows = $this->repository->findAllForListing($criteria);
$results = [];
foreach ($rows as $row) {
if ($row['uid'] === null) {
continue;
}
$pageId = (int)($row['pid'] ?? 0);
$uid = (int)$row['uid'];
if (!$this->permissionChecker->hasReadPermission($pageId)) {
continue;
}
$persistenceIdentifier = (string)$uid;
$hasWritePermission = $this->permissionChecker->hasWritePermission($pageId);
$metadata = new FormMetadata(
identifier: $row['identifier'] ?? '',
type: 'Form',
name: $row['label'] ?? $row['identifier'] ?? '',
prototypeName: 'standard',
persistenceIdentifier: $persistenceIdentifier,
readOnly: !$hasWritePermission,
removable: $hasWritePermission,
fileUid: null,
storageLocation: $this->getStorageLocationLabel(),
);
$results[] = $metadata;
}
return $results;
}
public function getFormManagerOptions(): array
{
if (!$this->permissionChecker->hasWritePermission(0)) {
return [];
}
return [
'allowedStorageLocations' => [
[
'value' => '0',
'label' => $this->getStorageLocationLabel(),
],
],
];
}
public function isAccessible(): bool
{
return $this->permissionChecker->hasWritePermission(0);
}
public function isAllowedStorageLocation(string $storageLocation): bool
{
if (MathUtility::canBeInterpretedAsInteger($storageLocation)) {
return (int)$storageLocation === 0;
}
return false;
}
public function isAllowedPersistenceIdentifier(string $persistenceIdentifier): bool
{
if (str_starts_with($persistenceIdentifier, 'NEW')) {
return true;
}
if (!MathUtility::canBeInterpretedAsInteger($persistenceIdentifier)) {
return false;
}
if (!$this->isAccessible()) {
return false;
}
$uid = (int)$persistenceIdentifier;
$record = $this->repository->findByUid($uid);
return $record !== null;
}
/**
* @throws PersistenceManagerException
*/
private function extractUidFromIdentifier(FormIdentifier $identifier): int
{
if (!MathUtility::canBeInterpretedAsInteger($identifier->identifier)) {
throw new PersistenceManagerException(
sprintf('Invalid database identifier "%s". Expected numeric UID.', $identifier->identifier),
1767199428
);
}
return (int)$identifier->identifier;
}
private function getStorageLocationLabel(): string
{
$languageService = $this->getLanguageService();
return $languageService?->sL('LLL:EXT:form/Resources/Private/Language/Database.xlf:' . $this->getLabel()) ?: 'Database';
}
private function getLanguageService(): ?LanguageService
{
return $GLOBALS['LANG'] ?? null;
}
}