diff --git a/typo3/sysext/core/Classes/Resource/ResourceStorage.php b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
index b2f3d0f05ecc..9ba88e3fd81b 100644
--- a/typo3/sysext/core/Classes/Resource/ResourceStorage.php
+++ b/typo3/sysext/core/Classes/Resource/ResourceStorage.php
@@ -1828,7 +1828,7 @@ public function moveFile($file, $targetFolder, $targetFileName = null, $conflict
throw new Exception\ExistingTargetFileNameException('The target file already exists', 1329850997);
}
}
- $this->emitPreFileMoveSignal($file, $targetFolder);
+ $this->emitPreFileMoveSignal($file, $targetFolder, $sanitizedTargetFileName);
$sourceStorage = $file->getStorage();
// Call driver method to move the file and update the index entry
try {
@@ -2501,10 +2501,11 @@ protected function emitPostFileCopySignal(FileInterface $file, Folder $targetFol
*
* @param FileInterface $file
* @param Folder $targetFolder
+ * @param string $targetFileName
*/
- protected function emitPreFileMoveSignal(FileInterface $file, Folder $targetFolder)
+ protected function emitPreFileMoveSignal(FileInterface $file, Folder $targetFolder, string $targetFileName)
{
- $this->getSignalSlotDispatcher()->dispatch(self::class, self::SIGNAL_PreFileMove, [$file, $targetFolder]);
+ $this->getSignalSlotDispatcher()->dispatch(self::class, self::SIGNAL_PreFileMove, [$file, $targetFolder, $targetFileName]);
}
/**
diff --git a/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php b/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
index b9466ac3bf2c..837571691e02 100644
--- a/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
+++ b/typo3/sysext/core/Classes/Utility/File/ExtendedFileUtility.php
@@ -1013,6 +1013,10 @@ public function func_edit($cmds)
$this->writeLog(9, 1, 100, 'File "%s" was not saved! File extension rejected!', [$fileObject->getIdentifier()]);
$this->addMessageToFlashMessageQueue('FileUtility.FileWasNotSaved', [$fileObject->getIdentifier()]);
return false;
+ } catch (\RuntimeException $e) {
+ $this->writeLog(9, 1, 100, 'File "%s" was not saved! File extension rejected!', [$fileObject->getIdentifier()]);
+ $this->addMessageToFlashMessageQueue('FileUtility.FileWasNotSaved', [$fileObject->getIdentifier()]);
+ return false;
}
}
diff --git a/typo3/sysext/core/Documentation/Changelog/8.7.x/Important-84910-DenyDirectFALCommandsForFormDefinitions.rst b/typo3/sysext/core/Documentation/Changelog/8.7.x/Important-84910-DenyDirectFALCommandsForFormDefinitions.rst
new file mode 100644
index 000000000000..dd4e3bdd3d65
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/8.7.x/Important-84910-DenyDirectFALCommandsForFormDefinitions.rst
@@ -0,0 +1,53 @@
+.. include:: ../../Includes.txt
+
+=================================================================
+Important: #84910 - Deny direct FAL commands for form definitions
+=================================================================
+
+In order to control settings in user provided form definitions files and only
+allow manipulations by using the backend form editor (or direct file access
+using e.g. SFTP) form file extensions have been changed form simple `.yaml`
+to more specific `.form.yaml`.
+
+Direct file commands by using either the backend file list module or implemented
+invocations of the file abstraction layer (FAL) API are denied per default and
+have to allowed explicitly for the following commands for files ending with the
+new file suffix `.form.yaml`:
+
+* plain command invocations
+ + create (creating new, empty file having `.form.yaml` suffix)
+ + rename (renaming to file having `.form.yaml` suffix)
+ + replace (replacing an existing file having `.form.yaml` suffix)
+ + move (moving to different file having `.form.yaml` suffix)
+* command and content invocations - content signature required
+ + add (uploading new file having `.form.yaml` suffix)
+ + setContents (changing contents of file having `.form.yaml` suffix)
+
+In order to grant those commands, `\TYPO3\CMS\Form\Slot\FilePersistenceSlot`
+has been introduced (singleton instance).
+
+.. code-block:: php
+
+ // Allowing content modifications on a $file object with
+ // given $newContent information prior to executing the command
+
+ $slot = GeneralUtility::makeInstance(FilePersistenceSlot::class);
+ $slot->allowInvocation(
+ FilePersistenceSlot::COMMAND_FILE_SET_CONTENTS,
+ $file->getCombinedIdentifier(),
+ $this->filePersistenceSlot->getContentSignature($newContent)
+ );
+
+ $file->setContents($newContent);
+
+In contrast to *plain command invocations*, those having *content invocations*
+(`add` and `setContents`, see list of commands above) require a content signature
+as well in order to be executed. The previous example demonstrates that for the
+`setContents` command.
+
+Extensions that are modifying (e.g. post-processing) persisted form definition
+files using the file abstraction layer (FAL) API need to adjust and extend their
+implementation and allow according invocations as outlined above.
+
+See :issue:`84910`
+.. index:: Backend, FAL, ext:form
diff --git a/typo3/sysext/form/Classes/Hooks/DataStructureIdentifierHook.php b/typo3/sysext/form/Classes/Hooks/DataStructureIdentifierHook.php
index fa775c59b490..4c3572b56af0 100644
--- a/typo3/sysext/form/Classes/Hooks/DataStructureIdentifierHook.php
+++ b/typo3/sysext/form/Classes/Hooks/DataStructureIdentifierHook.php
@@ -107,11 +107,18 @@ public function parseDataStructureByIdentifierPostProcess(array $dataStructure,
$formPersistenceManager = GeneralUtility::makeInstance(ObjectManager::class)->get(FormPersistenceManagerInterface::class);
$formIsAccessible = false;
foreach ($formPersistenceManager->listForms() as $form) {
+ $invalidFormDefinition = $form['invalid'] ?? false;
+ $hasDeprecatedFileExtension = $form['deprecatedFileExtension'] ?? false;
+
+ if ($form['location'] === 'storage' && $hasDeprecatedFileExtension) {
+ continue;
+ }
+
if ($form['persistenceIdentifier'] === $identifier['ext-form-persistenceIdentifier']) {
$formIsAccessible = true;
}
- if (isset($form['invalid']) && $form['invalid']) {
+ if ($invalidFormDefinition || $hasDeprecatedFileExtension) {
$dataStructure['sheets']['sDEF']['ROOT']['el']['settings.persistenceIdentifier']['TCEforms']['config']['items'][] = [
$form['name'] . ' (' . $form['persistenceIdentifier'] . ')',
$form['persistenceIdentifier'],
diff --git a/typo3/sysext/form/Classes/Hooks/FileListEditIconsHook.php b/typo3/sysext/form/Classes/Hooks/FileListEditIconsHook.php
new file mode 100644
index 000000000000..2bc5fa7404b6
--- /dev/null
+++ b/typo3/sysext/form/Classes/Hooks/FileListEditIconsHook.php
@@ -0,0 +1,51 @@
+getCombinedIdentifier();
+ $isFormDefinition = StringUtility::endsWith($fullIdentifier, FormPersistenceManager::FORM_DEFINITION_FILE_EXTENSION);
+
+ if (!$isFormDefinition) {
+ return;
+ }
+
+ $disableIconNames = ['edit', 'view', 'replace', 'rename'];
+ foreach ($disableIconNames as $disableIconName) {
+ if (!empty($cells[$disableIconName])) {
+ $cells[$disableIconName] = $parentObject->spaceIcon;
+ }
+ }
+ }
+}
diff --git a/typo3/sysext/form/Classes/Hooks/FormFileExtensionUpdate.php b/typo3/sysext/form/Classes/Hooks/FormFileExtensionUpdate.php
new file mode 100644
index 000000000000..aa73dcd835e0
--- /dev/null
+++ b/typo3/sysext/form/Classes/Hooks/FormFileExtensionUpdate.php
@@ -0,0 +1,390 @@
+getAllStorageFormFilesWithOldNaming();
+ $referencedExtensionFormFiles = $this->groupReferencedExtensionFormFiles(
+ $this->getReferencedFormFilesWithOldNaming()
+ );
+
+ $information = [];
+ if (count($allStorageFormFiles) > 0) {
+ $updateNeeded = true;
+ $information[] = 'Form configuration files were found that should be migrated to be named .form.yaml.';
+ }
+ if (count($referencedExtensionFormFiles) > 0) {
+ $updateNeeded = true;
+ $information[] = 'Referenced extension form configuration files found that should be updated.';
+ }
+ $description = implode('
', $information);
+
+ return $updateNeeded;
+ }
+
+ /**
+ * Performs the accordant updates.
+ *
+ * @param array &$dbQueries Queries done in this update
+ * @param string &$customMessage Custom message
+ * @return bool Whether everything went smoothly or not
+ */
+ public function performUpdate(array &$dbQueries, &$customMessage): bool
+ {
+ $messages = [];
+
+ $allStorageFormFiles = $this->getAllStorageFormFilesWithOldNaming();
+ $referencedFormFiles = $this->getReferencedFormFilesWithOldNaming();
+ $referencedExtensionFormFiles = $this->groupReferencedExtensionFormFiles($referencedFormFiles);
+ $filePersistenceSlot = GeneralUtility::makeInstance(FilePersistenceSlot::class);
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ $connection = $connectionPool->getConnectionForTable('tt_content');
+ $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
+ $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class);
+ $persistenceManager = $this->getObjectManager()->get(FormPersistenceManager::class);
+
+ $filePersistenceSlot->defineInvocation(
+ FilePersistenceSlot::COMMAND_FILE_RENAME,
+ true
+ );
+
+ // Processing all files in a regular file abstraction layer storage
+ foreach ($allStorageFormFiles as $file) {
+ $oldPersistenceIdentifier = $file->getCombinedIdentifier();
+
+ $newPossiblePersistenceIdentifier = $persistenceManager->getUniquePersistenceIdentifier(
+ $file->getNameWithoutExtension(),
+ $file->getParentFolder()->getCombinedIdentifier()
+ );
+ $newFileName = PathUtility::pathinfo(
+ $newPossiblePersistenceIdentifier,
+ PATHINFO_BASENAME
+ );
+
+ try {
+ $file->rename($newFileName, DuplicationBehavior::RENAME);
+ $newPersistenceIdentifier = $file->getCombinedIdentifier();
+ } catch (\Exception $e) {
+ $messages[] = sprintf(
+ 'Failed to rename identifier "%s" to "%s"',
+ $oldPersistenceIdentifier,
+ $newFileName
+ );
+ continue;
+ }
+
+ // Update referenced FlexForm in tt_content elements (if any)
+ $dataItems = $this->filterReferencedFormFilesByIdentifier(
+ $referencedFormFiles,
+ $oldPersistenceIdentifier
+ );
+ if (count($dataItems) === 0) {
+ continue;
+ }
+
+ foreach ($dataItems as $dataItem) {
+ // No reference index update needed since file UID not changed
+ $this->updateContentReference(
+ $connection,
+ $dataItem,
+ $oldPersistenceIdentifier,
+ $newPersistenceIdentifier
+ );
+ }
+ }
+
+ $filePersistenceSlot->defineInvocation(
+ FilePersistenceSlot::COMMAND_FILE_RENAME,
+ null
+ );
+
+ // Processing all referenced files being part of some extension
+ foreach ($referencedExtensionFormFiles as $identifier => $dataItems) {
+ $oldFilePath = GeneralUtility::getFileAbsFileName(
+ ltrim($identifier, '/')
+ );
+ $newFilePath = $this->upgradeFilename($oldFilePath);
+
+ if (!file_exists($newFilePath)) {
+ $messages[] = sprintf(
+ 'Failed to update content reference of identifier "0:%s"'
+ . ' (probably not renamed yet using ".form.yaml" suffix)',
+ $identifier
+ );
+ continue;
+ }
+
+ $oldExtensionIdentifier = preg_replace(
+ '#^/typo3conf/ext/#',
+ 'EXT:',
+ $identifier
+ );
+ $newExtensionIdentifier = $this->upgradeFilename(
+ $oldExtensionIdentifier
+ );
+
+ foreach ($dataItems as $dataItem) {
+ $result = $this->updateContentReference(
+ $connection,
+ $dataItem,
+ $oldExtensionIdentifier,
+ $newExtensionIdentifier
+ );
+ if (!$result) {
+ continue;
+ }
+ // Update reference index since extension file probably
+ // has been renamed or duplicated without invoking FAL API
+ $referenceIndex->updateRefIndexTable(
+ 'tt_content',
+ (int)$dataItem['recuid']
+ );
+ }
+ }
+
+ if (count($messages) > 0) {
+ $customMessage = 'The following issues occurred during performing updates:'
+ . '
- ' . implode('
- ', $messages) . '
';
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @param Connection $connection
+ * @param array $dataItem
+ * @param string $oldIdentifier
+ * @param string $newIdentifier
+ * @return bool
+ */
+ protected function updateContentReference(
+ Connection $connection,
+ array $dataItem,
+ string $oldIdentifier,
+ string $newIdentifier
+ ): bool {
+ if ($oldIdentifier === $newIdentifier) {
+ return false;
+ }
+
+ $flexForm = str_replace(
+ $oldIdentifier,
+ $newIdentifier,
+ $dataItem['pi_flexform']
+ );
+
+ $connection->update(
+ 'tt_content',
+ ['pi_flexform' => $flexForm],
+ ['uid' => (int)$dataItem['recuid']]
+ );
+
+ return true;
+ }
+
+ /**
+ * Upgrades filename to end with ".form.yaml", e.g.
+ * + "file.yaml" -> "file.form.yaml"
+ * + "file.form.yaml" -> "file.form.yaml" (unchanged)
+ *
+ * @param string $filename
+ * @return string
+ */
+ protected function upgradeFilename(string $filename): string
+ {
+ return preg_replace(
+ '#(?getObjectManager()
+ ->get(FormPersistenceManager::class);
+ $yamlSource = $this->getObjectManager()
+ ->get(YamlSource::class);
+
+ return array_filter(
+ $persistenceManager->retrieveYamlFilesFromStorageFolders(),
+ function (File $file) use ($yamlSource) {
+ $isNewFormFile = StringUtility::endsWith(
+ $file->getName(),
+ FormPersistenceManager::FORM_DEFINITION_FILE_EXTENSION
+ );
+ if ($isNewFormFile) {
+ return false;
+ }
+
+ try {
+ $form = $yamlSource->load([$file]);
+ return !empty($form['identifier'])
+ && ($form['type'] ?? null) === 'Form';
+ } catch (\Exception $exception) {
+ }
+ return false;
+ }
+ );
+ }
+
+ /**
+ * @return array
+ */
+ protected function getReferencedFormFilesWithOldNaming(): array
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('sys_refindex');
+ $queryBuilder->getRestrictions()->removeAll();
+ $records = $queryBuilder
+ ->select(
+ 'f.identifier AS identifier',
+ 'f.uid AS uid',
+ 'f.storage AS storage',
+ 'r.recuid AS recuid',
+ 't.pi_flexform AS pi_flexform'
+ )
+ ->from('sys_refindex', 'r')
+ ->innerJoin('r', 'sys_file', 'f', 'r.ref_uid = f.uid')
+ ->innerJoin('r', 'tt_content', 't', 'r.recuid = t.uid')
+ ->where(
+ $queryBuilder->expr()->eq(
+ 'r.ref_table',
+ $queryBuilder->createNamedParameter('sys_file', \PDO::PARAM_STR)
+ ),
+ $queryBuilder->expr()->eq(
+ 'r.softref_key',
+ $queryBuilder->createNamedParameter('formPersistenceIdentifier', \PDO::PARAM_STR)
+ ),
+ $queryBuilder->expr()->eq(
+ 'r.deleted',
+ $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+ ),
+ $queryBuilder->expr()->notLike(
+ 'f.identifier',
+ $queryBuilder->createNamedParameter('%.form.yaml', \PDO::PARAM_STR)
+ )
+ )
+ ->execute()
+ ->fetchAll();
+
+ return $records;
+ }
+
+ /**
+ * @param array $referencedFormFiles
+ * @return array
+ */
+ protected function groupReferencedExtensionFormFiles(
+ array $referencedFormFiles
+ ): array {
+ $referencedExtensionFormFiles = [];
+
+ foreach ($referencedFormFiles as $referencedFormFile) {
+ $identifier = $referencedFormFile['identifier'];
+ if ((int)$referencedFormFile['storage'] !== 0
+ || strpos($identifier, '/typo3conf/ext/') !== 0
+ ) {
+ continue;
+ }
+ $referencedExtensionFormFiles[$identifier][] = $referencedFormFile;
+ }
+
+ return $referencedExtensionFormFiles;
+ }
+
+ /**
+ * @param array $referencedFormFiles
+ * @param string $identifier
+ * @return array
+ */
+ protected function filterReferencedFormFilesByIdentifier(
+ array $referencedFormFiles,
+ string $identifier
+ ): array {
+ return array_filter(
+ $referencedFormFiles,
+ function (array $referencedFormFile) use ($identifier) {
+ $referencedFormFileIdentifier = sprintf(
+ '%d:%s',
+ $referencedFormFile['storage'],
+ $referencedFormFile['identifier']
+ );
+ return $referencedFormFileIdentifier === $identifier;
+ }
+ );
+ }
+
+ /**
+ * @param FolderInterface $folder
+ * @return string
+ */
+ protected function buildCombinedIdentifier(FolderInterface $folder): string
+ {
+ return sprintf(
+ '%d:%s',
+ $folder->getStorage()->getUid(),
+ $folder->getIdentifier()
+ );
+ }
+
+ /**
+ * @return ObjectManager
+ */
+ protected function getObjectManager()
+ {
+ return GeneralUtility::makeInstance(ObjectManager::class);
+ }
+}
diff --git a/typo3/sysext/form/Classes/Hooks/FormFileProvider.php b/typo3/sysext/form/Classes/Hooks/FormFileProvider.php
new file mode 100644
index 000000000000..bd60609b6846
--- /dev/null
+++ b/typo3/sysext/form/Classes/Hooks/FormFileProvider.php
@@ -0,0 +1,103 @@
+identifier, FormPersistenceManager::FORM_DEFINITION_FILE_EXTENSION);
+ }
+
+ /**
+ * @param array $items
+ * @return array
+ */
+ public function addItems(array $items): array
+ {
+ parent::initialize();
+ return $this->purgeItems($items);
+ }
+
+ /**
+ * Purges items that are not allowed for according command.
+ * According canBeEdited, canBeRenamed, ... commands will always return
+ * false in order to remove those form file items.
+ *
+ * Using the canRender() approach avoid adding hardcoded index name
+ * lookup. Thus, it's streamlined with the rest of the provides, but
+ * actually purges items instead of adding them.
+ *
+ * @param array $items
+ * @return array
+ */
+ protected function purgeItems(array $items): array
+ {
+ foreach ($items as $name => $item) {
+ $type = $item['type'];
+
+ if ($type === 'submenu' && !empty($item['childItems'])) {
+ $item['childItems'] = $this->purgeItems($item['childItems']);
+ } elseif (!parent::canRender($name, $type)) {
+ unset($items[$name]);
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function canBeEdited(): bool
+ {
+ return false;
+ }
+
+ /**
+ * @return bool
+ */
+ protected function canBeRenamed(): bool
+ {
+ return false;
+ }
+}
diff --git a/typo3/sysext/form/Classes/Hooks/FormPagePreviewRenderer.php b/typo3/sysext/form/Classes/Hooks/FormPagePreviewRenderer.php
index 3b5a4faef659..3fe0f5e8f656 100644
--- a/typo3/sysext/form/Classes/Hooks/FormPagePreviewRenderer.php
+++ b/typo3/sysext/form/Classes/Hooks/FormPagePreviewRenderer.php
@@ -22,11 +22,13 @@
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Service\FlexFormService;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\NoSuchFileException;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\ParseErrorException;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
+use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManager;
use TYPO3\CMS\Form\Mvc\Persistence\FormPersistenceManagerInterface;
/**
@@ -68,8 +70,18 @@ public function preProcess(
$formPersistenceManager = GeneralUtility::makeInstance(ObjectManager::class)->get(FormPersistenceManagerInterface::class);
try {
- $formDefinition = $formPersistenceManager->load($persistenceIdentifier);
- $formLabel = $formDefinition['label'];
+ if (
+ StringUtility::endsWith($persistenceIdentifier, FormPersistenceManager::FORM_DEFINITION_FILE_EXTENSION)
+ || strpos($persistenceIdentifier, 'EXT:') === 0
+ ) {
+ $formDefinition = $formPersistenceManager->load($persistenceIdentifier);
+ $formLabel = $formDefinition['label'];
+ } else {
+ $formLabel = sprintf(
+ $this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.inaccessiblePersistenceIdentifier'),
+ $persistenceIdentifier
+ );
+ }
} catch (ParseErrorException $e) {
$formLabel = sprintf(
$this->getLanguageService()->sL(self::L10N_PREFIX . 'tt_content.preview.invalidPersistenceIdentifier'),
diff --git a/typo3/sysext/form/Classes/Hooks/ImportExportHook.php b/typo3/sysext/form/Classes/Hooks/ImportExportHook.php
new file mode 100644
index 000000000000..ccebe43ac804
--- /dev/null
+++ b/typo3/sysext/form/Classes/Hooks/ImportExportHook.php
@@ -0,0 +1,41 @@
+allowInvocation(
+ FilePersistenceSlot::COMMAND_FILE_ADD,
+ implode(':', [$fileRecord['storage'], $fileRecord['identifier']]),
+ $formPersistenceSlot->getContentSignature(file_get_contents($temporaryFile))
+ );
+ }
+}
diff --git a/typo3/sysext/form/Classes/Mvc/Configuration/YamlSource.php b/typo3/sysext/form/Classes/Mvc/Configuration/YamlSource.php
index f2ea6d3b95da..9105fcfc1618 100644
--- a/typo3/sysext/form/Classes/Mvc/Configuration/YamlSource.php
+++ b/typo3/sysext/form/Classes/Mvc/Configuration/YamlSource.php
@@ -21,11 +21,13 @@
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\FolderInterface;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\FileWriteException;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\NoSuchFileException;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\ParseErrorException;
+use TYPO3\CMS\Form\Slot\FilePersistenceSlot;
/**
* Configuration source based on YAML files
@@ -44,6 +46,11 @@ class YamlSource
*/
protected $usePhpYamlExtension = false;
+ /**
+ * @var FilePersistenceSlot
+ */
+ protected $filePersistenceSlot;
+
/**
* Use PHP YAML Extension if installed.
* @internal
@@ -55,6 +62,14 @@ public function __construct()
}
}
+ /**
+ * @param FilePersistenceSlot $filePersistenceSlot
+ */
+ public function injectFilePersistenceSlot(FilePersistenceSlot $filePersistenceSlot)
+ {
+ $this->filePersistenceSlot = $filePersistenceSlot;
+ }
+
/**
* Loads the specified configuration files and returns its merged content
* as an array.
@@ -139,6 +154,16 @@ public function save($fileToSave, array $configuration)
if ($fileToSave instanceof File) {
try {
+ $this->filePersistenceSlot->allowInvocation(
+ FilePersistenceSlot::COMMAND_FILE_SET_CONTENTS,
+ $this->buildCombinedIdentifier(
+ $fileToSave->getParentFolder(),
+ $fileToSave->getName()
+ ),
+ $this->filePersistenceSlot->getContentSignature(
+ $header . LF . $yaml
+ )
+ );
$fileToSave->setContents($header . LF . $yaml);
} catch (InsufficientFileAccessPermissionsException $e) {
throw new FileWriteException($e->getMessage(), 1512582753, $e);
@@ -182,4 +207,19 @@ protected function getHeaderFromFile($file): string
}
return $header;
}
+
+ /**
+ * @param FolderInterface $folder
+ * @param string $fileName
+ * @return string
+ */
+ protected function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
+ {
+ return sprintf(
+ '%d:%s%s',
+ $folder->getStorage()->getUid(),
+ $folder->getIdentifier(),
+ $fileName
+ );
+ }
}
diff --git a/typo3/sysext/form/Classes/Mvc/Persistence/FormPersistenceManager.php b/typo3/sysext/form/Classes/Mvc/Persistence/FormPersistenceManager.php
index 869de1dee4db..49bdb9b04a1d 100644
--- a/typo3/sysext/form/Classes/Mvc/Persistence/FormPersistenceManager.php
+++ b/typo3/sysext/form/Classes/Mvc/Persistence/FormPersistenceManager.php
@@ -25,12 +25,14 @@
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Form\Mvc\Configuration\ConfigurationManagerInterface;
use TYPO3\CMS\Form\Mvc\Configuration\Exception\FileWriteException;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniqueIdentifierException;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\NoUniquePersistenceIdentifierException;
use TYPO3\CMS\Form\Mvc\Persistence\Exception\PersistenceManagerException;
+use TYPO3\CMS\Form\Slot\FilePersistenceSlot;
/**
* Concrete implementation of the FormPersistenceManagerInterface
@@ -39,6 +41,7 @@
*/
class FormPersistenceManager implements FormPersistenceManagerInterface
{
+ const FORM_DEFINITION_FILE_EXTENSION = '.form.yaml';
/**
* @var \TYPO3\CMS\Form\Mvc\Configuration\YamlSource
@@ -55,6 +58,11 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
*/
protected $formSettings;
+ /**
+ * @var FilePersistenceSlot
+ */
+ protected $filePersistenceSlot;
+
/**
* @param \TYPO3\CMS\Form\Mvc\Configuration\YamlSource $yamlSource
* @internal
@@ -73,6 +81,14 @@ public function injectStorageRepository(\TYPO3\CMS\Core\Resource\StorageReposito
$this->storageRepository = $storageRepository;
}
+ /**
+ * @param FilePersistenceSlot $filePersistenceSlot
+ */
+ public function injectFilePersistenceSlot(FilePersistenceSlot $filePersistenceSlot)
+ {
+ $this->filePersistenceSlot = $filePersistenceSlot;
+ }
+
/**
* @internal
*/
@@ -85,7 +101,9 @@ public function initializeObject()
/**
* Load the array formDefinition identified by $persistenceIdentifier, and return it.
- * Only files with the extension .yaml are loaded.
+ * Only files with the extension .yaml or .form.yaml are loaded.
+ * Form definition file names which not ends with ".form.yaml" has been
+ * deprecated in v9 and will not be supported in v10.
*
* @param string $persistenceIdentifier
* @return array
@@ -109,14 +127,28 @@ public function load(string $persistenceIdentifier): array
try {
$yaml = $this->yamlSource->load([$file]);
+
+ if (isset($yaml['identifier'], $yaml['type']) && $yaml['type'] === 'Form') {
+ if (
+ !$this->hasValidFileExtension($persistenceIdentifier)
+ && strpos($persistenceIdentifier, 'EXT:') === 0
+ ) {
+ trigger_error(
+ 'Form definition file name ("' . $persistenceIdentifier . '") which does not end with ".form.yaml" has been deprecated in v9 and will not be supported in v10.',
+ E_USER_DEPRECATED
+ );
+ } elseif (
+ !$this->hasValidFileExtension($persistenceIdentifier)
+ && strpos($persistenceIdentifier, 'EXT:') !== 0
+ ) {
+ throw new PersistenceManagerException(sprintf('Form definition "%s" does not end with ".form.yaml".', $persistenceIdentifier), 1531160649);
+ }
+ }
} catch (\Exception $e) {
$yaml = [
- 'identifier' => $file->getCombinedIdentifier(),
+ 'type' => 'Form',
+ 'identifier' => $persistenceIdentifier,
'label' => $e->getMessage(),
- 'error' => [
- 'code' => $e->getCode(),
- 'message' => $e->getMessage()
- ],
'invalid' => true,
];
}
@@ -126,7 +158,7 @@ public function load(string $persistenceIdentifier): array
/**
* Save the array form representation identified by $persistenceIdentifier.
- * Only files with the extension .yaml are saved.
+ * Only files with the extension .form.yaml are saved.
* If the formDefinition is located within a EXT: resource, save is only
* allowed if the configuration path
* TYPO3.CMS.Form.persistenceManager.allowSaveToExtensionPaths
@@ -139,7 +171,7 @@ public function load(string $persistenceIdentifier): array
*/
public function save(string $persistenceIdentifier, array $formDefinition)
{
- if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) !== 'yaml') {
+ if (!$this->hasValidFileExtension($persistenceIdentifier)) {
throw new PersistenceManagerException(sprintf('The file "%s" could not be saved.', $persistenceIdentifier), 1477679820);
}
@@ -168,7 +200,7 @@ public function save(string $persistenceIdentifier, array $formDefinition)
/**
* Delete the form representation identified by $persistenceIdentifier.
- * Only files with the extension .yaml are removed.
+ * Only files with the extension .form.yaml are removed.
*
* @param string $persistenceIdentifier
* @throws PersistenceManagerException
@@ -176,7 +208,7 @@ public function save(string $persistenceIdentifier, array $formDefinition)
*/
public function delete(string $persistenceIdentifier)
{
- if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) !== 'yaml') {
+ if (!$this->hasValidFileExtension($persistenceIdentifier)) {
throw new PersistenceManagerException(sprintf('The file "%s" could not be removed.', $persistenceIdentifier), 1472239534);
}
if (!$this->exists($persistenceIdentifier)) {
@@ -212,7 +244,7 @@ public function delete(string $persistenceIdentifier)
public function exists(string $persistenceIdentifier): bool
{
$exists = false;
- if (pathinfo($persistenceIdentifier, PATHINFO_EXTENSION) === 'yaml') {
+ if ($this->hasValidFileExtension($persistenceIdentifier)) {
if (strpos($persistenceIdentifier, 'EXT:') === 0) {
if (array_key_exists(pathinfo($persistenceIdentifier, PATHINFO_DIRNAME) . '/', $this->getAccessibleExtensionFolders())) {
$exists = file_exists(GeneralUtility::getFileAbsFileName($persistenceIdentifier));
@@ -238,43 +270,47 @@ public function exists(string $persistenceIdentifier): bool
*/
public function listForms(): array
{
- $fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class);
- $fileExtensionFilter->setAllowedFileExtensions(['yaml']);
-
$identifiers = [];
$forms = [];
- /** @var \TYPO3\CMS\Core\Resource\Folder $folder */
- foreach ($this->getAccessibleFormStorageFolders() as $folder) {
- $storage = $folder->getStorage();
- $storage->addFileAndFolderNameFilter([$fileExtensionFilter, 'filterFileList']);
+ foreach ($this->retrieveYamlFilesFromStorageFolders() as $file) {
+ /** @var Folder $folder */
+ $folder = $file->getParentFolder();
// TODO: deprecated since TYPO3 v9, will be removed in TYPO3 v10
- $formReadOnly = false;
- if ($folder->getCombinedIdentifier() === '1:/user_upload/') {
- $formReadOnly = true;
- }
+ $formReadOnly = $folder->getCombinedIdentifier() === '1:/user_upload/';
- $files = $folder->getFiles(0, 0, Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS, true);
- foreach ($files as $file) {
- $persistenceIdentifier = $storage->getUid() . ':' . $file->getIdentifier();
+ $persistenceIdentifier = $file->getCombinedIdentifier();
- $form = $this->load($persistenceIdentifier);
- if (isset($form['identifier'], $form['type']) && $form['type'] === 'Form') {
- $forms[] = [
- 'identifier' => $form['identifier'],
- 'name' => $form['label'] ?? $form['identifier'],
- 'persistenceIdentifier' => $persistenceIdentifier,
- 'readOnly' => $formReadOnly,
- 'removable' => true,
- 'location' => 'storage',
- 'duplicateIdentifier' => false,
- 'invalid' => $form['invalid'],
- 'error' => $form['error'],
- ];
- $identifiers[$form['identifier']]++;
- }
+ $form = $this->load($persistenceIdentifier);
+ if (empty($form['identifier']) || ($form['type'] ?? null) !== 'Form') {
+ continue;
+ }
+
+ if ($this->hasValidFileExtension($persistenceIdentifier)) {
+ $forms[] = [
+ 'identifier' => $form['identifier'],
+ 'name' => $form['label'] ?? $form['identifier'],
+ 'persistenceIdentifier' => $persistenceIdentifier,
+ 'readOnly' => $formReadOnly,
+ 'removable' => true,
+ 'location' => 'storage',
+ 'duplicateIdentifier' => false,
+ 'invalid' => $form['invalid'],
+ ];
+ $identifiers[$form['identifier']]++;
+ } else {
+ $forms[] = [
+ 'identifier' => $form['identifier'],
+ 'name' => $form['label'] ?? $form['identifier'],
+ 'persistenceIdentifier' => $persistenceIdentifier,
+ 'readOnly' => true,
+ 'removable' => false,
+ 'location' => 'storage',
+ 'duplicateIdentifier' => false,
+ 'invalid' => false,
+ 'deprecatedFileExtension' => true,
+ ];
}
- $storage->resetFileAndFolderNameFiltersToDefault();
}
foreach ($this->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
@@ -285,18 +321,31 @@ public function listForms(): array
}
$form = $this->load($relativePath . $fileInfo->getFilename());
if (isset($form['identifier'], $form['type']) && $form['type'] === 'Form') {
- $forms[] = [
- 'identifier' => $form['identifier'],
- 'name' => $form['label'] ?? $form['identifier'],
- 'persistenceIdentifier' => $relativePath . $fileInfo->getFilename(),
- 'readOnly' => $this->formSettings['persistenceManager']['allowSaveToExtensionPaths'] ? false: true,
- 'removable' => $this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths'] ? true: false,
- 'location' => 'extension',
- 'duplicateIdentifier' => false,
- 'invalid' => $form['invalid'],
- 'error' => $form['error'],
- ];
- $identifiers[$form['identifier']]++;
+ if ($this->hasValidFileExtension($fileInfo->getFilename())) {
+ $forms[] = [
+ 'identifier' => $form['identifier'],
+ 'name' => $form['label'] ?? $form['identifier'],
+ 'persistenceIdentifier' => $relativePath . $fileInfo->getFilename(),
+ 'readOnly' => $this->formSettings['persistenceManager']['allowSaveToExtensionPaths'] ? false: true,
+ 'removable' => $this->formSettings['persistenceManager']['allowDeleteFromExtensionPaths'] ? true: false,
+ 'location' => 'extension',
+ 'duplicateIdentifier' => false,
+ 'invalid' => $form['invalid'],
+ ];
+ $identifiers[$form['identifier']]++;
+ } else {
+ $forms[] = [
+ 'identifier' => $form['identifier'],
+ 'name' => $form['label'] ?? $form['identifier'],
+ 'persistenceIdentifier' => $relativePath . $fileInfo->getFilename(),
+ 'readOnly' => true,
+ 'removable' => false,
+ 'location' => 'extension',
+ 'duplicateIdentifier' => false,
+ 'invalid' => false,
+ 'deprecatedFileExtension' => true,
+ ];
+ }
}
}
}
@@ -314,6 +363,40 @@ public function listForms(): array
return $forms;
}
+ /**
+ * Retrieves yaml files from storage folders for further processing.
+ * At this time it's not determined yet, whether these files contain form data.
+ *
+ * @return File[]
+ * @internal
+ */
+ public function retrieveYamlFilesFromStorageFolders(): array
+ {
+ $filesFromStorageFolders = [];
+
+ $fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class);
+ $fileExtensionFilter->setAllowedFileExtensions(['yaml']);
+
+ foreach ($this->getAccessibleFormStorageFolders() as $folder) {
+ $storage = $folder->getStorage();
+ $storage->addFileAndFolderNameFilter([
+ $fileExtensionFilter,
+ 'filterFileList'
+ ]);
+
+ $files = $folder->getFiles(
+ 0,
+ 0,
+ Folder::FILTER_MODE_USE_OWN_AND_STORAGE_FILTERS,
+ true
+ );
+ $filesFromStorageFolders = $filesFromStorageFolders + $files;
+ $storage->resetFileAndFolderNameFiltersToDefault();
+ }
+
+ return $filesFromStorageFolders;
+ }
+
/**
* Return a list of all accessible file mountpoints for the
* current backend user.
@@ -409,17 +492,17 @@ public function getAccessibleExtensionFolders(): array
public function getUniquePersistenceIdentifier(string $formIdentifier, string $savePath): string
{
$savePath = rtrim($savePath, '/') . '/';
- $formPersistenceIdentifier = $savePath . $formIdentifier . '.yaml';
+ $formPersistenceIdentifier = $savePath . $formIdentifier . self::FORM_DEFINITION_FILE_EXTENSION;
if (!$this->exists($formPersistenceIdentifier)) {
return $formPersistenceIdentifier;
}
for ($attempts = 1; $attempts < 100; $attempts++) {
- $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, $attempts) . '.yaml';
+ $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, $attempts) . self::FORM_DEFINITION_FILE_EXTENSION;
if (!$this->exists($formPersistenceIdentifier)) {
return $formPersistenceIdentifier;
}
}
- $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, time()) . '.yaml';
+ $formPersistenceIdentifier = $savePath . sprintf('%s_%d', $formIdentifier, time()) . self::FORM_DEFINITION_FILE_EXTENSION;
if (!$this->exists($formPersistenceIdentifier)) {
return $formPersistenceIdentifier;
}
@@ -528,6 +611,10 @@ protected function getOrCreateFile(string $persistenceIdentifier): File
}
if (!$storage->hasFile($fileIdentifier)) {
+ $this->filePersistenceSlot->allowInvocation(
+ FilePersistenceSlot::COMMAND_FILE_CREATE,
+ $folder->getCombinedIdentifier() . $pathinfo['basename']
+ );
$file = $folder->createFile($pathinfo['basename']);
} else {
$file = $storage->getFile($fileIdentifier);
@@ -553,4 +640,13 @@ protected function getStorageByUid(int $storageUid): ResourceStorage
}
return $storage;
}
+
+ /**
+ * @param string $fileName
+ * @return bool
+ */
+ protected function hasValidFileExtension(string $fileName): bool
+ {
+ return StringUtility::endsWith($fileName, self::FORM_DEFINITION_FILE_EXTENSION);
+ }
}
diff --git a/typo3/sysext/form/Classes/Slot/FilePersistenceSlot.php b/typo3/sysext/form/Classes/Slot/FilePersistenceSlot.php
new file mode 100644
index 000000000000..7523deec95e2
--- /dev/null
+++ b/typo3/sysext/form/Classes/Slot/FilePersistenceSlot.php
@@ -0,0 +1,307 @@
+definedInvocations[$command] = $type;
+ if ($type === null) {
+ unset($this->definedInvocations[$command]);
+ }
+ }
+
+ /**
+ * Allows invocation for a particular combination of command and file
+ * identifier. Commands providing new content have have to submit a HMAC
+ * signature on the content as well.
+ *
+ * @param string $command
+ * @param string $combinedFileIdentifier
+ * @param string $contentSignature
+ * @return bool
+ * @see getContentSignature
+ */
+ public function allowInvocation(
+ string $command,
+ string $combinedFileIdentifier,
+ string $contentSignature = null
+ ): bool {
+ $index = $this->searchAllowedInvocation(
+ $command,
+ $combinedFileIdentifier,
+ $contentSignature
+ );
+
+ if ($index !== null) {
+ return false;
+ }
+
+ $this->allowedInvocations[] = [
+ 'command' => $command,
+ 'combinedFileIdentifier' => $combinedFileIdentifier,
+ 'contentSignature' => $contentSignature,
+ ];
+
+ return true;
+ }
+
+ /**
+ * @param string $fileName
+ * @param FolderInterface $targetFolder
+ */
+ public function onPreFileCreate(string $fileName, FolderInterface $targetFolder): void
+ {
+ $combinedFileIdentifier = $this->buildCombinedIdentifier(
+ $targetFolder,
+ $fileName
+ );
+
+ $this->assertFileName(
+ self::COMMAND_FILE_CREATE,
+ $combinedFileIdentifier
+ );
+ }
+
+ /**
+ * @param string $targetFileName
+ * @param FolderInterface $targetFolder
+ * @param string $sourceFilePath
+ */
+ public function onPreFileAdd(
+ string $targetFileName,
+ FolderInterface $targetFolder,
+ string $sourceFilePath
+ ): void {
+ $combinedFileIdentifier = $this->buildCombinedIdentifier(
+ $targetFolder,
+ $targetFileName
+ );
+ $this->assertFileName(
+ self::COMMAND_FILE_ADD,
+ $combinedFileIdentifier,
+ file_get_contents($sourceFilePath)
+ );
+ }
+
+ /**
+ * @param FileInterface $file
+ * @param string $targetFileName
+ */
+ public function onPreFileRename(FileInterface $file, string $targetFileName): void
+ {
+ $combinedFileIdentifier = $this->buildCombinedIdentifier(
+ $file->getParentFolder(),
+ $targetFileName
+ );
+
+ $this->assertFileName(
+ self::COMMAND_FILE_RENAME,
+ $combinedFileIdentifier
+ );
+ }
+
+ /**
+ * @param FileInterface $file
+ * @param string $localFilePath
+ */
+ public function onPreFileReplace(FileInterface $file, string $localFilePath): void
+ {
+ $combinedFileIdentifier = $this->buildCombinedIdentifier(
+ $file->getParentFolder(),
+ $file->getName()
+ );
+
+ $this->assertFileName(
+ self::COMMAND_FILE_REPLACE,
+ $combinedFileIdentifier
+ );
+ }
+
+ /**
+ * @param FileInterface $file
+ * @param FolderInterface $targetFolder
+ * @param string $targetFileName
+ */
+ public function onPreFileMove(FileInterface $file, FolderInterface $targetFolder, string $targetFileName): void
+ {
+ $combinedFileIdentifier = $this->buildCombinedIdentifier(
+ $targetFolder,
+ $targetFileName
+ );
+
+ $this->assertFileName(
+ self::COMMAND_FILE_MOVE,
+ $combinedFileIdentifier
+ );
+ }
+
+ /**
+ * @param FileInterface $file
+ * @param mixed $content
+ */
+ public function onPreFileSetContents(FileInterface $file, $content = null): void
+ {
+ $combinedFileIdentifier = $this->buildCombinedIdentifier(
+ $file->getParentFolder(),
+ $file->getName()
+ );
+
+ $this->assertFileName(
+ self::COMMAND_FILE_SET_CONTENTS,
+ $combinedFileIdentifier,
+ $content
+ );
+ }
+
+ /**
+ * @param string $command
+ * @param string $combinedFileIdentifier
+ * @param string $content
+ * @throws FormDefinitionPersistenceException
+ */
+ protected function assertFileName(
+ string $command,
+ string $combinedFileIdentifier,
+ string $content = null
+ ): void {
+ if (!StringUtility::endsWith($combinedFileIdentifier, FormPersistenceManager::FORM_DEFINITION_FILE_EXTENSION)) {
+ return;
+ }
+
+ $definedInvocation = $this->definedInvocations[$command] ?? null;
+ // whitelisted command
+ if ($definedInvocation === true) {
+ return;
+ }
+ // blacklisted command
+ if ($definedInvocation === false) {
+ throw new FormDefinitionPersistenceException(
+ sprintf(
+ 'Persisting form definition "%s" is denied',
+ $combinedFileIdentifier
+ ),
+ 1530281201
+ );
+ }
+
+ $contentSignature = null;
+ if ($content !== null) {
+ $contentSignature = $this->getContentSignature((string)$content);
+ }
+ $allowedInvocationIndex = $this->searchAllowedInvocation(
+ $command,
+ $combinedFileIdentifier,
+ $contentSignature
+ );
+
+ if ($allowedInvocationIndex === null) {
+ throw new FormDefinitionPersistenceException(
+ sprintf(
+ 'Persisting form definition "%s" is denied',
+ $combinedFileIdentifier
+ ),
+ 1530281202
+ );
+ }
+ unset($this->allowedInvocations[$allowedInvocationIndex]);
+ }
+
+ /**
+ * @param string $command
+ * @param string $combinedFileIdentifier
+ * @param string|null $contentSignature
+ * @return int|null
+ */
+ protected function searchAllowedInvocation(
+ string $command,
+ string $combinedFileIdentifier,
+ string $contentSignature = null
+ ): ?int {
+ foreach ($this->allowedInvocations as $index => $allowedInvocation) {
+ if (
+ $command === $allowedInvocation['command']
+ && $combinedFileIdentifier === $allowedInvocation['combinedFileIdentifier']
+ && $contentSignature === $allowedInvocation['contentSignature']
+ ) {
+ return $index;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param FolderInterface $folder
+ * @param string $fileName
+ * @return string
+ */
+ protected function buildCombinedIdentifier(FolderInterface $folder, string $fileName): string
+ {
+ return sprintf(
+ '%d:%s%s',
+ $folder->getStorage()->getUid(),
+ $folder->getIdentifier(),
+ $fileName
+ );
+ }
+}
diff --git a/typo3/sysext/form/Classes/Slot/FormDefinitionPersistenceException.php b/typo3/sysext/form/Classes/Slot/FormDefinitionPersistenceException.php
new file mode 100644
index 000000000000..e828ecf99e4d
--- /dev/null
+++ b/typo3/sysext/form/Classes/Slot/FormDefinitionPersistenceException.php
@@ -0,0 +1,23 @@
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/typo3/sysext/form/Resources/Private/Language/Database.xlf b/typo3/sysext/form/Resources/Private/Language/Database.xlf
index be29c85d5b2b..f1afef187277 100644
--- a/typo3/sysext/form/Resources/Private/Language/Database.xlf
+++ b/typo3/sysext/form/Resources/Private/Language/Database.xlf
@@ -203,6 +203,9 @@
+
+
+
diff --git a/typo3/sysext/form/Tests/Unit/Hooks/DataStructureIdentifierHookTest.php b/typo3/sysext/form/Tests/Unit/Hooks/DataStructureIdentifierHookTest.php
index 48106a2d1143..1f8a9745325c 100644
--- a/typo3/sysext/form/Tests/Unit/Hooks/DataStructureIdentifierHookTest.php
+++ b/typo3/sysext/form/Tests/Unit/Hooks/DataStructureIdentifierHookTest.php
@@ -252,6 +252,7 @@ public function parseDataStructureByIdentifierPostProcessDataProvider(): array
[
'persistenceIdentifier' => 'hugo1',
'name' => 'myHugo1',
+ 'location' => 'extension',
],
[
'myHugo1 (hugo1)',
@@ -264,6 +265,7 @@ public function parseDataStructureByIdentifierPostProcessDataProvider(): array
'persistenceIdentifier' => 'Error.yaml',
'label' => 'Test Error Label',
'name' => 'Test Error Name',
+ 'location' => 'extension',
'invalid' => true,
],
[
diff --git a/typo3/sysext/form/Tests/Unit/Mvc/Persistence/Fixtures/BlankForm.yaml b/typo3/sysext/form/Tests/Unit/Mvc/Persistence/Fixtures/BlankForm.form.yaml
similarity index 100%
rename from typo3/sysext/form/Tests/Unit/Mvc/Persistence/Fixtures/BlankForm.yaml
rename to typo3/sysext/form/Tests/Unit/Mvc/Persistence/Fixtures/BlankForm.form.yaml
diff --git a/typo3/sysext/form/Tests/Unit/Mvc/Persistence/FormPersistenceManagerTest.php b/typo3/sysext/form/Tests/Unit/Mvc/Persistence/FormPersistenceManagerTest.php
index 252c698a6b8a..0826601f9d61 100644
--- a/typo3/sysext/form/Tests/Unit/Mvc/Persistence/FormPersistenceManagerTest.php
+++ b/typo3/sysext/form/Tests/Unit/Mvc/Persistence/FormPersistenceManagerTest.php
@@ -62,7 +62,7 @@ public function loadThrowsExceptionIfPersistenceIdentifierIsAExtensionLocationWh
],
]);
- $input = 'EXT:form/Resources/Forms/_example.yaml';
+ $input = 'EXT:form/Resources/Forms/_example.form.yaml';
$mockFormPersistenceManager->_call('load', $input);
}
@@ -100,7 +100,7 @@ public function saveThrowsExceptionIfPersistenceIdentifierIsAExtensionLocationAn
],
]);
- $input = 'EXT:form/Resources/Forms/_example.yaml';
+ $input = 'EXT:form/Resources/Forms/_example.form.yaml';
$mockFormPersistenceManager->_call('save', $input, []);
}
@@ -123,7 +123,7 @@ public function saveThrowsExceptionIfPersistenceIdentifierIsAExtensionLocationWh
],
]);
- $input = 'EXT:form/Resources/Forms/_example.yaml';
+ $input = 'EXT:form/Resources/Forms/_example.form.yaml';
$mockFormPersistenceManager->_call('save', $input, []);
}
@@ -160,7 +160,7 @@ public function deleteThrowsExceptionIfPersistenceIdentifierFileDoesNotExists():
->method('exists')
->willReturn(false);
- $input = '-1:/user_uploads/_example.yaml';
+ $input = '-1:/user_uploads/_example.form.yaml';
$mockFormPersistenceManager->_call('delete', $input);
}
@@ -187,7 +187,7 @@ public function deleteThrowsExceptionIfPersistenceIdentifierIsExtensionLocationA
],
]);
- $input = 'EXT:form/Resources/Forms/_example.yaml';
+ $input = 'EXT:form/Resources/Forms/_example.form.yaml';
$mockFormPersistenceManager->_call('delete', $input);
}
@@ -215,7 +215,7 @@ public function deleteThrowsExceptionIfPersistenceIdentifierIsExtensionLocationW
],
]);
- $input = 'EXT:form/Resources/Forms/_example.yaml';
+ $input = 'EXT:form/Resources/Forms/_example.form.yaml';
$mockFormPersistenceManager->_call('delete', $input);
}
@@ -257,7 +257,7 @@ public function deleteThrowsExceptionIfPersistenceIdentifierIsStorageLocationAnd
->method('exists')
->willReturn(true);
- $input = '-1:/user_uploads/_example.yaml';
+ $input = '-1:/user_uploads/_example.form.yaml';
$mockFormPersistenceManager->_call('delete', $input);
}
@@ -278,7 +278,7 @@ public function existsReturnsTrueIfPersistenceIdentifierIsExtensionLocationAndFi
],
]);
- $input = 'EXT:form/Tests/Unit/Mvc/Persistence/Fixtures/BlankForm.yaml';
+ $input = 'EXT:form/Tests/Unit/Mvc/Persistence/Fixtures/BlankForm.form.yaml';
$this->assertTrue($mockFormPersistenceManager->_call('exists', $input));
}
@@ -349,7 +349,7 @@ public function existsReturnsTrueIfPersistenceIdentifierIsStorageLocationAndFile
->method('getStorageByUid')
->willReturn($mockStorage);
- $input = '-1:/user_uploads/_example.yaml';
+ $input = '-1:/user_uploads/_example.form.yaml';
$this->assertTrue($mockFormPersistenceManager->_call('exists', $input));
}
@@ -430,7 +430,7 @@ public function getUniquePersistenceIdentifierAppendNumberIfPersistenceIdentifie
->willReturn(false);
$input = 'example';
- $expected = '-1:/user_uploads/example_2.yaml';
+ $expected = '-1:/user_uploads/example_2.form.yaml';
$this->assertSame($expected, $mockFormPersistenceManager->_call('getUniquePersistenceIdentifier', $input, '-1:/user_uploads/'));
}
@@ -456,7 +456,7 @@ public function getUniquePersistenceIdentifierAppendTimestampIfPersistenceIdenti
->willReturn(false);
$input = 'example';
- $expected = '#^-1:/user_uploads/example_([0-9]{10}).yaml$#';
+ $expected = '#^-1:/user_uploads/example_([0-9]{10}).form.yaml$#';
$returnValue = $mockFormPersistenceManager->_call('getUniquePersistenceIdentifier', $input, '-1:/user_uploads/');
$this->assertEquals(1, preg_match($expected, $returnValue));
diff --git a/typo3/sysext/form/ext_localconf.php b/typo3/sysext/form/ext_localconf.php
index f0224f5cb224..888e262caef8 100644
--- a/typo3/sysext/form/ext_localconf.php
+++ b/typo3/sysext/form/ext_localconf.php
@@ -2,6 +2,21 @@
defined('TYPO3_MODE') or die();
call_user_func(function () {
+ // Register upgrade wizard in install tool
+ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['formFileExtension']
+ = \TYPO3\CMS\Form\Hooks\FormFileExtensionUpdate::class;
+
+ // Context menu item handling for form files
+ $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'][1530637161]
+ = \TYPO3\CMS\Form\Hooks\FormFileProvider::class;
+
+ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/impexp/class.tx_impexp.php']['before_addSysFileRecord'][1530637161]
+ = \TYPO3\CMS\Form\Hooks\ImportExportHook::class . '->beforeAddSysFileRecordOnImport';
+
+ // File list edit icons
+ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['fileList']['editIconsHook'][1530637161]
+ = \TYPO3\CMS\Form\Hooks\FileListEditIconsHook::class;
+
// Hook to enrich tt_content form flex element with finisher settings and form list drop down
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][\TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools::class]['flexParsing'][
\TYPO3\CMS\Form\Hooks\DataStructureIdentifierHook::class
@@ -69,4 +84,45 @@
['FormFrontend' => 'perform'],
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
);
+
+ // Register slots for file handling
+ $signalSlotDispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
+ \TYPO3\CMS\Extbase\SignalSlot\Dispatcher::class
+ );
+ $signalSlotDispatcher->connect(
+ \TYPO3\CMS\Core\Resource\ResourceStorage::class,
+ \TYPO3\CMS\Core\Resource\ResourceStorageInterface::SIGNAL_PreFileCreate,
+ \TYPO3\CMS\Form\Slot\FilePersistenceSlot::class,
+ 'onPreFileCreate'
+ );
+ $signalSlotDispatcher->connect(
+ \TYPO3\CMS\Core\Resource\ResourceStorage::class,
+ \TYPO3\CMS\Core\Resource\ResourceStorageInterface::SIGNAL_PreFileAdd,
+ \TYPO3\CMS\Form\Slot\FilePersistenceSlot::class,
+ 'onPreFileAdd'
+ );
+ $signalSlotDispatcher->connect(
+ \TYPO3\CMS\Core\Resource\ResourceStorage::class,
+ \TYPO3\CMS\Core\Resource\ResourceStorageInterface::SIGNAL_PreFileRename,
+ \TYPO3\CMS\Form\Slot\FilePersistenceSlot::class,
+ 'onPreFileRename'
+ );
+ $signalSlotDispatcher->connect(
+ \TYPO3\CMS\Core\Resource\ResourceStorage::class,
+ \TYPO3\CMS\Core\Resource\ResourceStorageInterface::SIGNAL_PreFileReplace,
+ \TYPO3\CMS\Form\Slot\FilePersistenceSlot::class,
+ 'onPreFileReplace'
+ );
+ $signalSlotDispatcher->connect(
+ \TYPO3\CMS\Core\Resource\ResourceStorage::class,
+ \TYPO3\CMS\Core\Resource\ResourceStorageInterface::SIGNAL_PreFileMove,
+ \TYPO3\CMS\Form\Slot\FilePersistenceSlot::class,
+ 'onPreFileMove'
+ );
+ $signalSlotDispatcher->connect(
+ \TYPO3\CMS\Core\Resource\ResourceStorage::class,
+ \TYPO3\CMS\Core\Resource\ResourceStorageInterface::SIGNAL_PreFileSetContents,
+ \TYPO3\CMS\Form\Slot\FilePersistenceSlot::class,
+ 'onPreFileSetContents'
+ );
});
diff --git a/typo3/sysext/impexp/Classes/Import.php b/typo3/sysext/impexp/Classes/Import.php
index e1c6622037f6..273be67fb997 100644
--- a/typo3/sysext/impexp/Classes/Import.php
+++ b/typo3/sysext/impexp/Classes/Import.php
@@ -429,6 +429,12 @@ protected function writeSysFileRecords()
$importFolder = $storage->getFolder($folderName);
}
+ $this->callHook('before_addSysFileRecord', [
+ 'fileRecord' => $fileRecord,
+ 'importFolder' => $importFolder,
+ 'temporaryFile' => $temporaryFile
+ ]);
+
try {
/** @var $newFile File */
$newFile = $storage->addFile($temporaryFile, $importFolder, $fileRecord['name']);