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:' + . '
'; + 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 @@ Duplicate identifier! + + Unsupported file extension! + Standard 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']);