405 lines
15 KiB
PHP
405 lines
15 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\Mvc\Persistence;
|
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
|
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
|
use TYPO3\CMS\Form\Domain\DTO\FormData;
|
|
use TYPO3\CMS\Form\Domain\DTO\FormMetadata;
|
|
use TYPO3\CMS\Form\Domain\DTO\PersistenceManagerConfiguration;
|
|
use TYPO3\CMS\Form\Domain\DTO\SearchCriteria;
|
|
use TYPO3\CMS\Form\Domain\DTO\StorageContext;
|
|
use TYPO3\CMS\Form\Domain\ValueObject\FormIdentifier;
|
|
use TYPO3\CMS\Form\Mvc\Configuration\TypoScriptService;
|
|
use TYPO3\CMS\Form\Mvc\Persistence\Event\AfterFormDefinitionLoadedEvent;
|
|
use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniqueIdentifierException;
|
|
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
|
|
use TYPO3\CMS\Form\Service\DatabaseService;
|
|
use TYPO3\CMS\Form\Storage\StorageAdapterFactory;
|
|
|
|
/**
|
|
* Form Persistence Manager - Entry point for FormManagerController
|
|
*
|
|
* This manager acts as a facade that delegates storage operations to appropriate
|
|
* storage adapters via the StorageAdapterFactory.
|
|
*
|
|
* Scope: frontend / backend
|
|
* @internal
|
|
*/
|
|
#[AsAlias(FormPersistenceManagerInterface::class)]
|
|
readonly class FormPersistenceManager implements FormPersistenceManagerInterface
|
|
{
|
|
public function __construct(
|
|
private StorageAdapterFactory $storageAdapterFactory,
|
|
#[Autowire(service: 'cache.runtime')]
|
|
private FrontendInterface $runtimeCache,
|
|
private EventDispatcherInterface $eventDispatcher,
|
|
private TypoScriptService $typoScriptService,
|
|
private DatabaseService $databaseService,
|
|
) {}
|
|
|
|
/**
|
|
* Load form definition and apply event listeners and TypoScript overrides
|
|
*/
|
|
public function load(string $persistenceIdentifier, ?array $typoScriptSettings = null, ?ServerRequestInterface $request = null): array
|
|
{
|
|
$cacheKey = 'ext-form-load-' . hash('xxh3', $persistenceIdentifier);
|
|
|
|
if ($this->runtimeCache->has($cacheKey)) {
|
|
$formDefinition = $this->runtimeCache->get($cacheKey);
|
|
} else {
|
|
$formDefinition = $this->loadFromStorage($persistenceIdentifier, $request);
|
|
$this->runtimeCache->set($cacheKey, $formDefinition);
|
|
}
|
|
|
|
$formDefinition = $this->eventDispatcher
|
|
->dispatch(new AfterFormDefinitionLoadedEvent($formDefinition, $persistenceIdentifier, $cacheKey))
|
|
->getFormDefinition();
|
|
|
|
if ($request !== null && !empty($typoScriptSettings['formDefinitionOverrides'][$formDefinition['identifier']] ?? null)) {
|
|
$formDefinitionOverrides = $this->typoScriptService->resolvePossibleTypoScriptConfiguration(
|
|
$typoScriptSettings['formDefinitionOverrides'][$formDefinition['identifier']],
|
|
$request
|
|
);
|
|
ArrayUtility::mergeRecursiveWithOverrule($formDefinition, $formDefinitionOverrides);
|
|
}
|
|
|
|
return $formDefinition;
|
|
}
|
|
|
|
/**
|
|
* Save form definition to appropriate storage
|
|
*
|
|
* @throws PersistenceManagerException
|
|
*/
|
|
public function save(string $persistenceIdentifier, array $formDefinition, array $formSettings, ?string $storageLocation = null): FormIdentifier
|
|
{
|
|
if (!$this->isAllowedPersistenceIdentifier($persistenceIdentifier)) {
|
|
throw new PersistenceManagerException(
|
|
sprintf('Save to path "%s" is not allowed.', $persistenceIdentifier),
|
|
1477680881
|
|
);
|
|
}
|
|
|
|
$identifier = new FormIdentifier($persistenceIdentifier);
|
|
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
|
$formData = FormData::fromArray($formDefinition);
|
|
|
|
$context = null;
|
|
if (MathUtility::canBeInterpretedAsInteger($storageLocation)) {
|
|
$context = StorageContext::create((int)$storageLocation);
|
|
}
|
|
|
|
$savedIdentifier = $adapter->write($identifier, $formData, $context);
|
|
|
|
$this->clearFormCache($savedIdentifier->identifier);
|
|
return $savedIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Delete form definition from storage
|
|
*/
|
|
public function delete(string $persistenceIdentifier, array $formSettings): void
|
|
{
|
|
$identifier = new FormIdentifier($persistenceIdentifier);
|
|
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
|
|
|
if (!$adapter->exists($identifier)) {
|
|
throw new PersistenceManagerException(
|
|
sprintf('The form "%s" does not exist.', $persistenceIdentifier),
|
|
1472239535
|
|
);
|
|
}
|
|
|
|
$adapter->delete($identifier);
|
|
$this->clearFormCache($persistenceIdentifier);
|
|
}
|
|
|
|
/**
|
|
* List all form definitions from all available storages
|
|
*/
|
|
public function listForms(array $formSettings, SearchCriteria $searchCriteria): array
|
|
{
|
|
$identifiers = [];
|
|
$forms = [];
|
|
|
|
foreach ($this->storageAdapterFactory->getAllAdapters() as $adapter) {
|
|
try {
|
|
$formDataList = $adapter->findAll($searchCriteria);
|
|
|
|
foreach ($formDataList as $formData) {
|
|
if ($formData->storageType === null) {
|
|
$formData = $formData->withStorageType($adapter->getTypeIdentifier());
|
|
}
|
|
$forms[] = $formData;
|
|
if (!isset($identifiers[$formData->identifier])) {
|
|
$identifiers[$formData->identifier] = 0;
|
|
}
|
|
$identifiers[$formData->identifier]++;
|
|
}
|
|
} catch (\Exception $e) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
foreach ($identifiers as $identifier => $count) {
|
|
if ($count > 1) {
|
|
foreach ($forms as $index => $formMetadata) {
|
|
if ($formMetadata->identifier === $identifier) {
|
|
$forms[$index] = $formMetadata->withDuplicateIdentifier(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$allReferencesForFileUid = $this->databaseService->getAllReferencesForFileUid();
|
|
$allReferencesForPersistenceIdentifier = $this->databaseService->getAllReferencesForPersistenceIdentifier();
|
|
$allReferencesForFormDefinitionUid = $this->databaseService->getAllReferencesForFormDefinitionUid();
|
|
|
|
foreach ($forms as $index => $formMetadata) {
|
|
if (isset($formMetadata->fileUid) && array_key_exists($formMetadata->fileUid, $allReferencesForFileUid)) {
|
|
$referenceCount = $allReferencesForFileUid[$formMetadata->fileUid];
|
|
} elseif ($formMetadata->persistenceIdentifier && array_key_exists($formMetadata->persistenceIdentifier, $allReferencesForFormDefinitionUid)) {
|
|
$referenceCount = $allReferencesForFormDefinitionUid[$formMetadata->persistenceIdentifier];
|
|
} elseif ($formMetadata->persistenceIdentifier && array_key_exists($formMetadata->persistenceIdentifier, $allReferencesForPersistenceIdentifier)) {
|
|
$referenceCount = $allReferencesForPersistenceIdentifier[$formMetadata->persistenceIdentifier];
|
|
} else {
|
|
$referenceCount = 0;
|
|
}
|
|
if ($referenceCount > 0) {
|
|
$forms[$index] = $formMetadata->withReferenceCount($referenceCount);
|
|
}
|
|
}
|
|
|
|
return $this->sortForms($forms, $formSettings, $searchCriteria->getOrderField(), $searchCriteria->getOrderDirection());
|
|
}
|
|
|
|
/**
|
|
* Check if any forms are available
|
|
*/
|
|
public function hasForms(array $formSettings): bool
|
|
{
|
|
foreach ($this->storageAdapterFactory->getAllAdapters() as $adapter) {
|
|
try {
|
|
$forms = $adapter->findAll(new SearchCriteria(limit: 1));
|
|
|
|
if (!empty($forms)) {
|
|
return true;
|
|
}
|
|
} catch (\Exception) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get unique persistence identifier for a new form
|
|
*/
|
|
public function getUniquePersistenceIdentifier(string $storage, string $formIdentifier, ?string $savePath): string
|
|
{
|
|
return $this->storageAdapterFactory->getAdapterByType($storage)->getUniquePersistenceIdentifier($formIdentifier, $savePath);
|
|
}
|
|
|
|
/**
|
|
* Get unique identifier (not persistence identifier)
|
|
*/
|
|
public function getUniqueIdentifier(string $identifier): string
|
|
{
|
|
$originalIdentifier = $identifier;
|
|
|
|
if ($this->checkForDuplicateIdentifier($identifier)) {
|
|
for ($attempts = 1; $attempts < 100; $attempts++) {
|
|
$identifier = sprintf('%s_%d', $originalIdentifier, $attempts);
|
|
if (!$this->checkForDuplicateIdentifier($identifier)) {
|
|
return $identifier;
|
|
}
|
|
}
|
|
|
|
$identifier = $originalIdentifier . '_' . time();
|
|
if ($this->checkForDuplicateIdentifier($identifier)) {
|
|
throw new NoUniqueIdentifierException(
|
|
sprintf('Could not find a unique identifier for form identifier "%s" after %d attempts', $identifier, $attempts),
|
|
1477688567
|
|
);
|
|
}
|
|
}
|
|
|
|
return $identifier;
|
|
}
|
|
|
|
/**
|
|
* Check if a storage location is allowed
|
|
*
|
|
* For database storage: storageLocation is a PID
|
|
* For file storage: storageLocation is a folder path (e.g., "1:/forms/")
|
|
*/
|
|
public function isAllowedStorageLocation(string $storageLocation): bool
|
|
{
|
|
try {
|
|
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($storageLocation);
|
|
return $adapter->isAllowedStorageLocation($storageLocation);
|
|
} catch (\Exception) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a persistence identifier is allowed
|
|
*
|
|
* For database storage: identifier is a UID or NEW*
|
|
* For file storage: identifier is a full file path (e.g., "1:/forms/contact.form.yaml")
|
|
*/
|
|
public function isAllowedPersistenceIdentifier(string $persistenceIdentifier): bool
|
|
{
|
|
try {
|
|
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
|
return $adapter->isAllowedPersistenceIdentifier($persistenceIdentifier);
|
|
} catch (\Exception) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if file has valid extension
|
|
*/
|
|
public function hasValidFileExtension(string $fileName): bool
|
|
{
|
|
return str_ends_with($fileName, self::FORM_DEFINITION_FILE_EXTENSION);
|
|
}
|
|
|
|
/**
|
|
* Load form definition from storage
|
|
*/
|
|
private function loadFromStorage(string $persistenceIdentifier, ?ServerRequestInterface $request = null): array
|
|
{
|
|
try {
|
|
$identifier = new FormIdentifier($persistenceIdentifier);
|
|
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
|
|
|
return $adapter->read($identifier, $request)->toArray();
|
|
} catch (\Exception $e) {
|
|
return [
|
|
'type' => 'Form',
|
|
'identifier' => $persistenceIdentifier,
|
|
'label' => $e->getMessage(),
|
|
'invalid' => true,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if form with persistence identifier exists
|
|
*/
|
|
private function exists(string $persistenceIdentifier): bool
|
|
{
|
|
try {
|
|
$identifier = new FormIdentifier($persistenceIdentifier);
|
|
$adapter = $this->storageAdapterFactory->getAdapterForIdentifier($persistenceIdentifier);
|
|
|
|
return $adapter->exists($identifier);
|
|
} catch (\Exception) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a form with given identifier already exists in any storage
|
|
*/
|
|
private function checkForDuplicateIdentifier(string $identifier): bool
|
|
{
|
|
foreach ($this->storageAdapterFactory->getAllAdapters() as $adapter) {
|
|
try {
|
|
if ($adapter->existsByFormIdentifier($identifier)) {
|
|
return true;
|
|
}
|
|
} catch (\Exception) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function sortForms(array $forms, array $formSettings, string $orderField = '', ?string $orderDirection = null): array
|
|
{
|
|
$persistenceConfiguration = PersistenceManagerConfiguration::fromArray($formSettings['persistenceManager'] ?? []);
|
|
if ($orderDirection) {
|
|
$ascending = $orderDirection === 'asc';
|
|
} else {
|
|
$ascending = $persistenceConfiguration->sortAscending;
|
|
}
|
|
$sortMultiplier = $ascending ? 1 : -1;
|
|
$keys = $orderField ? [$orderField] : $persistenceConfiguration->sortByKeys;
|
|
|
|
usort($forms, static function (FormMetadata $a, FormMetadata $b) use ($keys, $sortMultiplier) {
|
|
foreach ($keys as $key) {
|
|
$aValue = $a->getSortableValue($key);
|
|
$bValue = $b->getSortableValue($key);
|
|
|
|
if ($aValue === null || $bValue === null) {
|
|
continue;
|
|
}
|
|
|
|
$diff = (is_int($aValue) && is_int($bValue))
|
|
? $aValue - $bValue
|
|
: strcasecmp((string)$aValue, (string)$bValue);
|
|
|
|
if ($diff !== 0) {
|
|
return $diff * $sortMultiplier;
|
|
}
|
|
}
|
|
return 0;
|
|
});
|
|
return $forms;
|
|
}
|
|
|
|
/**
|
|
* Clear cache for specific form
|
|
*/
|
|
private function clearFormCache(string $persistenceIdentifier): void
|
|
{
|
|
$cacheKey = 'ext-form-load-' . hash('xxh3', $persistenceIdentifier);
|
|
$this->runtimeCache->remove($cacheKey);
|
|
}
|
|
|
|
public function getAccessibleStorageAdapters(): array
|
|
{
|
|
$storageAdapters = [];
|
|
foreach ($this->storageAdapterFactory->getAllAdapters() as $adapter) {
|
|
if ($adapter->isAccessible() === false) {
|
|
continue;
|
|
}
|
|
$storageAdapters[] = [
|
|
'typeIdentifier' => $adapter->getTypeIdentifier(),
|
|
'label' => $adapter->getLabel(),
|
|
'description' => $adapter->getDescription(),
|
|
'iconIdentifier' => $adapter->getIconIdentifier(),
|
|
'options' => $adapter->getFormManagerOptions(),
|
|
];
|
|
}
|
|
return $storageAdapters;
|
|
}
|
|
}
|