[BUGFIX] The form upgrade wizard must update all plugin settings 31/57731/4
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Thu, 12 Jul 2018 20:48:42 +0000 (22:48 +0200)
committerOliver Hader <oliver.hader@typo3.org>
Mon, 30 Jul 2018 21:45:23 +0000 (23:45 +0200)
The form definition renaming upgrade wizard renames the persistence
identifier within the form plugin flexform.
As a result, finisher overrides can no longer be properly assigned.
This patch adds an upgrade wizard which will be able to restore these
finisher overrides.

Resolves: #85544
Releases: master, 8.7
Change-Id: Idf1ffd8432fed88431b9a0feb407f42df3304401
Reviewed-on: https://review.typo3.org/57731
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
typo3/sysext/form/Classes/Hooks/FormFileExtensionUpdate.php
typo3/sysext/form/Classes/Mvc/Persistence/FormPersistenceManager.php
typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Form/legacy.yaml [new file with mode: 0644]
typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Form/updated.form.yaml [new file with mode: 0644]
typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Yaml/AllowedExtensionPaths.yaml [new file with mode: 0644]
typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/ext_emconf.php [new file with mode: 0644]
typo3/sysext/form/Tests/Functional/Hooks/FormFileExtensionUpdateTest.php [new file with mode: 0644]

index 1fe853e..0ac1c13 100644 (file)
@@ -15,12 +15,14 @@ namespace TYPO3\CMS\Form\Hooks;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Database\ReferenceIndex;
 use TYPO3\CMS\Core\Resource\DuplicationBehavior;
 use TYPO3\CMS\Core\Resource\File;
-use TYPO3\CMS\Core\Resource\FolderInterface;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\PathUtility;
 use TYPO3\CMS\Core\Utility\StringUtility;
@@ -36,12 +38,43 @@ use TYPO3\CMS\Lang\LanguageService;
  */
 class FormFileExtensionUpdate extends AbstractUpdate
 {
+
     /**
      * @var string
      */
     protected $title = 'Rename form definition file extension from .yaml to .form.yaml';
 
     /**
+     * @var FormPersistenceManager
+     */
+    protected $persistenceManager;
+
+    /**
+     * @var YamlSource
+     */
+    protected $yamlSource;
+
+    /**
+     * @var ResourceFactory
+     */
+    protected $resourceFactory;
+
+    /**
+     * @var ReferenceIndex
+     */
+    protected $referenceIndex;
+
+    /**
+     * @var FlexFormTools
+     */
+    protected $flexFormTools;
+
+    /**
+     * @var Connection
+     */
+    protected $connection;
+
+    /**
      * Checks whether updates are required.
      *
      * @param string &$description The description for the update
@@ -50,21 +83,65 @@ class FormFileExtensionUpdate extends AbstractUpdate
     public function checkForUpdate(&$description)
     {
         $updateNeeded = false;
+        $information = [];
 
-        $allStorageFormFiles = $this->getAllStorageFormFilesWithOldNaming();
-        $referencedExtensionFormFiles = $this->groupReferencedExtensionFormFiles(
-            $this->getReferencedFormFilesWithOldNaming()
-        );
+        $this->persistenceManager = $this->getObjectManager()->get(FormPersistenceManager::class);
+        $this->yamlSource = $this->getObjectManager()->get(YamlSource::class);
+        $this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
 
-        $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.';
+        foreach ($this->getFormDefinitionsInformation() as $formDefinitionInformation) {
+            if (
+                (
+                    $formDefinitionInformation['hasNewFileExtension'] === true
+                    && $formDefinitionInformation['hasReferencesForOldFileExtension'] === false
+                    && $formDefinitionInformation['hasReferencesForNewFileExtension'] === false
+                )
+                || (
+                    $formDefinitionInformation['hasNewFileExtension'] === false
+                    && $formDefinitionInformation['location'] === 'extension'
+                    && $formDefinitionInformation['hasReferencesForOldFileExtension'] === false
+                    && $formDefinitionInformation['hasReferencesForNewFileExtension'] === false
+                )
+            ) {
+                continue;
+            }
+
+            if (
+                $formDefinitionInformation['hasNewFileExtension'] === false
+                && $formDefinitionInformation['location'] === 'storage'
+            ) {
+                $updateNeeded = true;
+                $information['rename'] = 'Form definition files were found that should be migrated to be named .form.yaml.';
+            }
+
+            if (
+                $formDefinitionInformation['hasNewFileExtension']
+                && $formDefinitionInformation['hasReferencesForOldFileExtension']
+            ) {
+                $updateNeeded = true;
+                $information['updateReference'] = 'Referenced form definition files found that should be updated.';
+            }
+
+            if (
+                $formDefinitionInformation['referencesForOldFileExtensionNeedsFlexformUpdates'] === true
+                || $formDefinitionInformation['referencesForNewFileExtensionNeedsFlexformUpdates'] === true
+            ) {
+                $updateNeeded = true;
+                if ($formDefinitionInformation['hasNewFileExtension'] === true) {
+                    $information['updateReference'] = 'Referenced form definition files found that should be updated.';
+                } else {
+                    if ($formDefinitionInformation['location'] === 'storage') {
+                        $information['updateReference'] = 'Referenced form definition files found that should be updated.';
+                    } else {
+                        $information['manualStepsNeeded'] =
+                            'There are references to form definitions which are located in extensions and thus cannot be renamed automatically by this wizard.'
+                          . 'This form definitions from extensions that do not end with .form.yaml have to be renamed by hand!'
+                          . 'After that you can run this wizard again to migrate the references.';
+                    }
+                }
+            }
         }
+
         $description = implode('<br>', $information);
 
         return $updateNeeded;
@@ -81,62 +158,146 @@ class FormFileExtensionUpdate extends AbstractUpdate
     {
         $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);
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $filePersistenceSlot = GeneralUtility::makeInstance(FilePersistenceSlot::class);
+
+        $this->connection = $connectionPool->getConnectionForTable('tt_content');
+        $this->persistenceManager = $this->getObjectManager()->get(FormPersistenceManager::class);
+        $this->yamlSource = $this->getObjectManager()->get(YamlSource::class);
+        $this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+        $this->referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
+        $this->flexFormTools = GeneralUtility::makeInstance(FlexFormTools::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
-                );
+        $formDefinitionsInformation = $this->getFormDefinitionsInformation();
+        foreach ($formDefinitionsInformation as $currentPersistenceIdentifier => $formDefinitionInformation) {
+            if (
+                (
+                    $formDefinitionInformation['hasNewFileExtension'] === true
+                    && $formDefinitionInformation['hasReferencesForOldFileExtension'] === false
+                    && $formDefinitionInformation['hasReferencesForNewFileExtension'] === false
+                )
+                || (
+                    $formDefinitionInformation['hasNewFileExtension'] === false
+                    && $formDefinitionInformation['location'] === 'extension'
+                    && $formDefinitionInformation['hasReferencesForOldFileExtension'] === false
+                    && $formDefinitionInformation['hasReferencesForNewFileExtension'] === false
+                )
+            ) {
                 continue;
             }
 
-            // Update referenced FlexForm in tt_content elements (if any)
-            $dataItems = $this->filterReferencedFormFilesByIdentifier(
-                $referencedFormFiles,
-                $oldPersistenceIdentifier
-            );
-            if (count($dataItems) === 0) {
+            if (
+                $formDefinitionInformation['hasNewFileExtension'] === true
+                && (
+                    $formDefinitionInformation['hasReferencesForOldFileExtension'] === true
+                    || $formDefinitionInformation['hasReferencesForNewFileExtension'] === true
+                )
+            ) {
+                foreach ($formDefinitionInformation['referencesForOldFileExtension'] as $referenceForOldFileExtension) {
+                    $newFlexformXml = $this->generateNewFlexformForReference(
+                        $referenceForOldFileExtension,
+                        $referenceForOldFileExtension['sheetIdentifiersWhichNeedsUpdate'],
+                        $formDefinitionInformation['persistenceIdentifier']
+                    );
+                    $this->updateContentReference(
+                        $referenceForOldFileExtension['ttContentUid'],
+                        $newFlexformXml,
+                        true
+                    );
+                }
+
+                foreach ($formDefinitionInformation['referencesForNewFileExtension'] as $referenceForNewFileExtension) {
+                    $newFlexformXml = $this->generateNewFlexformForReference(
+                        $referenceForNewFileExtension,
+                        $referenceForNewFileExtension['sheetIdentifiersWhichNeedsUpdate']
+                    );
+                    $this->updateContentReference(
+                        $referenceForNewFileExtension['ttContentUid'],
+                        $newFlexformXml
+                    );
+                }
+
                 continue;
             }
 
-            foreach ($dataItems as $dataItem) {
-                // No reference index update needed since file UID not changed
-                $this->updateContentReference(
-                    $connection,
-                    $dataItem,
-                    $oldPersistenceIdentifier,
-                    $newPersistenceIdentifier
+            if ($formDefinitionInformation['location'] === 'storage') {
+                $file = $formDefinitionInformation['file'];
+
+                $newPossiblePersistenceIdentifier = $this->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 form definition "%s" to "%s".',
+                        $formDefinitionInformation['persistenceIdentifier'],
+                        $newFileName
+                    );
+                    continue;
+                }
+
+                if (
+                    $formDefinitionInformation['hasReferencesForOldFileExtension'] === true
+                    || $formDefinitionInformation['hasReferencesForNewFileExtension'] === true
+                ) {
+                    foreach ($formDefinitionInformation['referencesForOldFileExtension'] as $referenceForOldFileExtension) {
+                        $sheetIdentifiersWhichNeedsUpdate = $this->getSheetIdentifiersWhichNeedsUpdate(
+                            $referenceForOldFileExtension['flexform'],
+                            $formDefinitionsInformation,
+                            $currentPersistenceIdentifier,
+                            $formDefinitionInformation['persistenceIdentifier'],
+                            $newPersistenceIdentifier
+                        );
+                        $newFlexformXml = $this->generateNewFlexformForReference(
+                            $referenceForOldFileExtension,
+                            $sheetIdentifiersWhichNeedsUpdate,
+                            $newPersistenceIdentifier
+                        );
+                        $this->updateContentReference(
+                            $referenceForOldFileExtension['ttContentUid'],
+                            $newFlexformXml
+                        );
+                    }
+
+                    foreach ($formDefinitionInformation['referencesForNewFileExtension'] as $referenceForNewFileExtension) {
+                        $sheetIdentifiersWhichNeedsUpdate = $this->getSheetIdentifiersWhichNeedsUpdate(
+                            $referenceForNewFileExtension['flexform'],
+                            $formDefinitionsInformation,
+                            $currentPersistenceIdentifier,
+                            $formDefinitionInformation['persistenceIdentifier'],
+                            $newPersistenceIdentifier
+                        );
+                        $newFlexformXml = $this->generateNewFlexformForReference(
+                            $referenceForNewFileExtension,
+                            $sheetIdentifiersWhichNeedsUpdate,
+                            $newPersistenceIdentifier
+                        );
+                        $this->updateContentReference(
+                            $referenceForNewFileExtension['ttContentUid'],
+                            $newFlexformXml
+                        );
+                    }
+                }
+            } else {
+                $messages[] = sprintf(
+                    'Failed to rename form definition "%s" to "%s". You have to be rename it by hand!. '
+                  . 'After that you can run this wizard again to migrate the references.',
+                    $formDefinitionInformation['persistenceIdentifier'],
+                    $this->getNewPersistenceIdentifier($formDefinitionInformation['persistenceIdentifier'])
                 );
             }
         }
@@ -146,175 +307,413 @@ class FormFileExtensionUpdate extends AbstractUpdate
             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 (count($messages) > 0) {
+            $customMessage = 'The following issues occurred during performing updates:'
+                . '<br><ul><li>' . implode('</li><li>', $messages) . '</li></ul>';
+            return false;
+        }
 
-            if (!file_exists($newFilePath)) {
-                $messages[] = sprintf(
-                    'Failed to update content reference of identifier "0:%s"'
-                    . ' (probably not renamed yet using ".form.yaml" suffix)',
-                    $identifier
-                );
+        return true;
+    }
+
+    /**
+     * @return array
+     */
+    protected function getFormDefinitionsInformation(): array
+    {
+        $formDefinitionsInformation = array_merge(
+            $this->getFormDefinitionsInformationFromStorages(),
+            $this->getFormDefinitionsInformationFromExtensions()
+        );
+
+        $formDefinitionsInformation = $this->enrichFormDefinitionsInformationWithDataFromReferences($formDefinitionsInformation);
+
+        return $formDefinitionsInformation;
+    }
+
+    /**
+     * @return array
+     */
+    protected function getFormDefinitionsInformationFromStorages(): array
+    {
+        $formDefinitionsInformation =  [];
+
+        foreach ($this->persistenceManager->retrieveYamlFilesFromStorageFolders() as $file) {
+            $persistenceIdentifier = $file->getCombinedIdentifier();
+
+            $formDefinition = $this->getFormDefinition($file);
+            if (empty($formDefinition)) {
                 continue;
             }
 
-            $oldExtensionIdentifier = preg_replace(
-                '#^/typo3conf/ext/#',
-                'EXT:',
-                $identifier
+            $formDefinitionsInformation[$persistenceIdentifier] = $this->setFormDefinitionInformationData(
+                $persistenceIdentifier,
+                $formDefinition,
+                $file,
+                'storage'
             );
-            $newExtensionIdentifier = $this->upgradeFilename(
-                $oldExtensionIdentifier
+        }
+
+        return $formDefinitionsInformation;
+    }
+
+    /**
+     * @return array
+     */
+    protected function getFormDefinitionsInformationFromExtensions(): array
+    {
+        $formDefinitionsInformation =  [];
+
+        foreach ($this->persistenceManager->retrieveYamlFilesFromExtensionFolders() as $persistenceIdentifier => $_) {
+            try {
+                $file = $this->resourceFactory->retrieveFileOrFolderObject($persistenceIdentifier);
+            } catch (\Exception $exception) {
+                continue;
+            }
+
+            $formDefinition = $this->getFormDefinition($file);
+            if (empty($formDefinition)) {
+                continue;
+            }
+
+            $formDefinitionsInformation[$persistenceIdentifier] = $this->setFormDefinitionInformationData(
+                $persistenceIdentifier,
+                $formDefinition,
+                $file,
+                'extension'
             );
+        }
 
-            foreach ($dataItems as $dataItem) {
-                $result = $this->updateContentReference(
-                    $connection,
-                    $dataItem,
-                    $oldExtensionIdentifier,
-                    $newExtensionIdentifier
-                );
-                if (!$result) {
-                    continue;
+        return $formDefinitionsInformation;
+    }
+
+    /**
+     * @param string $persistenceIdentifier
+     * @param array $formDefinition
+     * @param File $file
+     * @param string $localtion
+     * @return array
+     */
+    protected function setFormDefinitionInformationData(
+        string $persistenceIdentifier,
+        array $formDefinition,
+        File $file,
+        string $localtion
+    ): array {
+        return [
+            'location' => $localtion,
+            'persistenceIdentifier' => $persistenceIdentifier,
+            'prototypeName' => $formDefinition['prototypeName'],
+            'formIdentifier' => $formDefinition['identifier'],
+            'file' => $file,
+            'referencesForOldFileExtension' => [],
+            'referencesForNewFileExtension' => [],
+            'hasNewFileExtension' => $this->hasNewFileExtension($persistenceIdentifier),
+            'hasReferencesForOldFileExtension' => false,
+            'hasReferencesForNewFileExtension' => false,
+            'referencesForOldFileExtensionNeedsFlexformUpdates' => false,
+            'referencesForNewFileExtensionNeedsFlexformUpdates' => false,
+        ];
+    }
+
+    /**
+     * @param array $formDefinitionsInformation
+     * @return array
+     */
+    protected function enrichFormDefinitionsInformationWithDataFromReferences(array $formDefinitionsInformation): array
+    {
+        foreach ($this->getAllFlexformFieldsFromFormPlugins() as $pluginData) {
+            if (empty($pluginData['pi_flexform'])) {
+                continue;
+            }
+            $flexform = GeneralUtility::xml2array($pluginData['pi_flexform']);
+            $referencedPersistenceIdentifier = $this->getPersistenceIdentifierFromFlexform($flexform);
+            $referenceHasNewFileExtension = $this->hasNewFileExtension($referencedPersistenceIdentifier);
+            $possibleOldReferencedPersistenceIdentifier = $this->getOldPersistenceIdentifier($referencedPersistenceIdentifier);
+            $possibleNewReferencedPersistenceIdentifier = $this->getNewPersistenceIdentifier($referencedPersistenceIdentifier);
+
+            $referenceData = [
+                'scope' => null,
+                'ttContentUid' => (int)$pluginData['uid'],
+                'flexform' => $flexform,
+                'sheetIdentifiersWhichNeedsUpdate' => [],
+            ];
+
+            $targetPersistenceIdentifier = null;
+            if (array_key_exists($referencedPersistenceIdentifier, $formDefinitionsInformation)) {
+                $targetPersistenceIdentifier = $referencedPersistenceIdentifier;
+                if ($referenceHasNewFileExtension) {
+                    $referenceData['scope'] = 'referencesForNewFileExtension';
+                } else {
+                    $referenceData['scope'] = 'referencesForOldFileExtension';
                 }
-                // Update reference index since extension file probably
-                // has been renamed or duplicated without invoking FAL API
-                $referenceIndex->updateRefIndexTable(
-                    'tt_content',
-                    (int)$dataItem['recuid']
-                );
+            } else {
+                if ($referenceHasNewFileExtension) {
+                    if (array_key_exists($possibleOldReferencedPersistenceIdentifier, $formDefinitionsInformation)) {
+                        $targetPersistenceIdentifier = $possibleOldReferencedPersistenceIdentifier;
+                        $referenceData['scope'] = 'referencesForNewFileExtension';
+                    } else {
+                        // There is no existing file for this reference
+                        continue;
+                    }
+                } else {
+                    if (array_key_exists($possibleNewReferencedPersistenceIdentifier, $formDefinitionsInformation)) {
+                        $targetPersistenceIdentifier = $possibleNewReferencedPersistenceIdentifier;
+                        $referenceData['scope'] = 'referencesForOldFileExtension';
+                    } else {
+                        // There is no existing file for this reference
+                        continue;
+                    }
+                }
+            }
+
+            $referenceData['sheetIdentifiersWhichNeedsUpdate'] = $this->getSheetIdentifiersWhichNeedsUpdate(
+                $flexform,
+                $formDefinitionsInformation,
+                $targetPersistenceIdentifier,
+                $possibleOldReferencedPersistenceIdentifier,
+                $possibleNewReferencedPersistenceIdentifier
+            );
+
+            $scope = $referenceData['scope'];
+
+            $formDefinitionsInformation[$targetPersistenceIdentifier][$scope][] = $referenceData;
+            if ($scope === 'referencesForOldFileExtension') {
+                $formDefinitionsInformation[$targetPersistenceIdentifier]['hasReferencesForOldFileExtension'] = true;
+                $formDefinitionsInformation[$targetPersistenceIdentifier]['referencesForOldFileExtensionNeedsFlexformUpdates'] = !empty($referenceData['sheetIdentifiersWhichNeedsUpdate']);
+            } else {
+                $formDefinitionsInformation[$targetPersistenceIdentifier]['hasReferencesForNewFileExtension'] = true;
+                $formDefinitionsInformation[$targetPersistenceIdentifier]['referencesForNewFileExtensionNeedsFlexformUpdates'] = !empty($referenceData['sheetIdentifiersWhichNeedsUpdate']);
             }
         }
 
-        if (count($messages) > 0) {
-            $customMessage = 'The following issues occurred during performing updates:'
-                . '<br><ul><li>' . implode('</li><li>', $messages) . '</li></ul>';
-            return false;
+        return $formDefinitionsInformation;
+    }
+
+    /**
+     * @param array $flexform
+     * @param array $formDefinitionsInformation
+     * @param string $targetPersistenceIdentifier
+     * @param string $possibleOldReferencedPersistenceIdentifier
+     * @param string $possibleNewReferencedPersistenceIdentifier
+     * @return array
+     */
+    protected function getSheetIdentifiersWhichNeedsUpdate(
+        array $flexform,
+        array $formDefinitionsInformation,
+        string $targetPersistenceIdentifier,
+        string $possibleOldReferencedPersistenceIdentifier,
+        string $possibleNewReferencedPersistenceIdentifier
+    ): array {
+        $sheetIdentifiersWhichNeedsUpdate = [];
+
+        $sheetIdentifiers = $this->getSheetIdentifiersForFinisherOverrides($flexform);
+        foreach ($sheetIdentifiers as $currentSheetIdentifier => $finisherIdentifier) {
+            $sheetIdentifierForOldPersistenceIdentifier = $this->buildExpectedSheetIdentifier(
+                $possibleOldReferencedPersistenceIdentifier,
+                $formDefinitionsInformation[$targetPersistenceIdentifier]['prototypeName'],
+                $formDefinitionsInformation[$targetPersistenceIdentifier]['formIdentifier'],
+                $finisherIdentifier
+            );
+
+            $sheetIdentifierForNewPersistenceIdentifier = $this->buildExpectedSheetIdentifier(
+                $possibleNewReferencedPersistenceIdentifier,
+                $formDefinitionsInformation[$targetPersistenceIdentifier]['prototypeName'],
+                $formDefinitionsInformation[$targetPersistenceIdentifier]['formIdentifier'],
+                $finisherIdentifier
+            );
+
+            if (
+                $currentSheetIdentifier === $sheetIdentifierForOldPersistenceIdentifier
+                && !array_key_exists($sheetIdentifierForNewPersistenceIdentifier, $sheetIdentifiers)
+            ) {
+                $sheetIdentifiersWhichNeedsUpdate[$currentSheetIdentifier] = $sheetIdentifierForNewPersistenceIdentifier;
+            }
         }
 
-        return true;
+        return $sheetIdentifiersWhichNeedsUpdate;
     }
 
     /**
-     * @param Connection $connection
-     * @param array $dataItem
-     * @param string $oldIdentifier
-     * @param string $newIdentifier
-     * @return bool
+     * @param array $flexform
+     * @return array
      */
-    protected function updateContentReference(
-        Connection $connection,
-        array $dataItem,
-        string $oldIdentifier,
-        string $newIdentifier
-    ): bool {
-        if ($oldIdentifier === $newIdentifier) {
-            return false;
+    protected function getSheetIdentifiersForFinisherOverrides(array $flexform): array
+    {
+        $sheetIdentifiers = [];
+        foreach ($this->getFinisherSheetsFromFlexform($flexform) as $sheetIdentifier => $sheetData) {
+            $firstSheetItemOptionPath = array_shift(array_keys($sheetData['lDEF']));
+            preg_match('#^settings\.finishers\.(.*)\..+$#', $firstSheetItemOptionPath, $matches);
+            if (!isset($matches[1])) {
+                continue;
+            }
+            $finisherIdentifier = $matches[1];
+            $sheetIdentifiers[$sheetIdentifier] = $finisherIdentifier;
+        }
+
+        return $sheetIdentifiers;
+    }
+
+    /**
+     * @param array $flexform
+     * @return array
+     */
+    protected function getFinisherSheetsFromFlexform(array $flexform): array
+    {
+        if (!isset($flexform['data'])) {
+            return [];
         }
 
-        $flexForm = str_replace(
-            $oldIdentifier,
-            $newIdentifier,
-            $dataItem['pi_flexform']
+        return array_filter(
+            $flexform['data'],
+            function ($key) {
+                return $key !== 'sDEF' && strlen($key) === 32;
+            },
+            ARRAY_FILTER_USE_KEY
         );
+    }
 
-        $connection->update(
-            'tt_content',
-            ['pi_flexform' => $flexForm],
-            ['uid' => (int)$dataItem['recuid']]
+    /**
+     * @param array $flexform
+     * @return string
+     */
+    protected function getPersistenceIdentifierFromFlexform(array $flexform): string
+    {
+        return $flexform['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF'] ?? '';
+    }
+
+    /**
+     * @param array $referenceData
+     * @param array $sheetIdentifiersWhichNeedsUpdate
+     * @param string $newPersistenceIdentifier
+     * @return string
+     */
+    protected function generateNewFlexformForReference(
+        array $referenceData,
+        array $sheetIdentifiersWhichNeedsUpdate,
+        string $newPersistenceIdentifier = ''
+    ): string {
+        $flexform = $referenceData['flexform'];
+        if (!empty($newPersistenceIdentifier)) {
+            $flexform['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF'] = $newPersistenceIdentifier;
+        }
+
+        foreach ($sheetIdentifiersWhichNeedsUpdate as $oldSheetIdentifier => $newSheetIdentifier) {
+            $flexform['data'][$newSheetIdentifier] = $flexform['data'][$oldSheetIdentifier];
+            unset($flexform['data'][$oldSheetIdentifier]);
+        }
+
+        return $this->flexFormTools->flexArray2Xml($flexform, true);
+    }
+
+    /**
+     * @param string $persistenceIdentifier
+     * @return bool
+     */
+    protected function hasNewFileExtension(string $persistenceIdentifier): bool
+    {
+        return StringUtility::endsWith(
+            $persistenceIdentifier,
+            FormPersistenceManager::FORM_DEFINITION_FILE_EXTENSION
         );
+    }
 
-        return true;
+    /**
+     * @param array $formDefinition
+     * @return bool
+     */
+    protected function looksLikeAFormDefinition(array $formDefinition): bool
+    {
+        return isset($formDefinition['identifier'], $formDefinition['type']) && $formDefinition['type'] === 'Form';
     }
 
     /**
-     * Upgrades filename to end with ".form.yaml", e.g.
-     * + "file.yaml"      -> "file.form.yaml"
-     * + "file.form.yaml" -> "file.form.yaml" (unchanged)
-     *
-     * @param string $filename
+     * @param string $persistenceIdentifier
      * @return string
      */
-    protected function upgradeFilename(string $filename): string
+    protected function getOldPersistenceIdentifier(string $persistenceIdentifier): string
+    {
+        return preg_replace(
+            '
+            #^(.*)(\.form\.yaml)$#',
+            '${1}.yaml',
+            $persistenceIdentifier
+        );
+    }
+
+    /**
+     * @param string $persistenceIdentifier
+     * @return string
+     */
+    protected function getNewPersistenceIdentifier(string $persistenceIdentifier): string
     {
         return preg_replace(
             '#(?<!\.form).yaml$#',
             '.form.yaml',
-            $filename
+            $persistenceIdentifier
         );
     }
 
     /**
-     * @return File[]
+     * @param string $persistenceIdentifier
+     * @param string $prototypeName
+     * @param string $formIdentifier
+     * @param string $finisherIdentifier
+     * @return string
      */
-    protected function getAllStorageFormFilesWithOldNaming(): array
-    {
-        $persistenceManager = $this->getObjectManager()
-            ->get(FormPersistenceManager::class);
-        $yamlSource = $this->getObjectManager()
-            ->get(YamlSource::class);
+    protected function buildExpectedSheetIdentifier(
+        string $persistenceIdentifier,
+        string $prototypeName,
+        string $formIdentifier,
+        string $finisherIdentifier
+    ): string {
+        return md5(
+            implode('', [
+                $persistenceIdentifier,
+                $prototypeName,
+                $formIdentifier,
+                $finisherIdentifier
+            ])
+        );
+    }
 
-        return array_filter(
-            $persistenceManager->retrieveYamlFilesFromStorageFolders(),
-            function (File $file) use ($yamlSource) {
-                $isNewFormFile = StringUtility::endsWith(
-                    $file->getName(),
-                    FormPersistenceManager::FORM_DEFINITION_FILE_EXTENSION
-                );
-                if ($isNewFormFile) {
-                    return false;
-                }
+    /**
+     * @param File $file
+     * @return array
+     */
+    protected function getFormDefinition(File $file): array
+    {
+        try {
+            $formDefinition = $this->yamlSource->load([$file]);
 
-                try {
-                    $form = $yamlSource->load([$file]);
-                    return !empty($form['identifier'])
-                        && ($form['type'] ?? null) === 'Form';
-                } catch (\Exception $exception) {
-                }
-                return false;
+            if (!$this->looksLikeAFormDefinition($formDefinition)) {
+                $formDefinition = [];
             }
-        );
+        } catch (\Exception $exception) {
+            $formDefinition = [];
+        }
+
+        return $formDefinition;
     }
 
     /**
      * @return array
      */
-    protected function getReferencedFormFilesWithOldNaming(): array
+    protected function getAllFlexformFieldsFromFormPlugins(): array
     {
         $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-            ->getQueryBuilderForTable('sys_refindex');
-        $queryBuilder->getRestrictions()->removeAll();
+            ->getQueryBuilderForTable('tt_content');
+        $queryBuilder->getRestrictions()
+            ->removeAll()
+            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+
         $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')
+            ->select('uid', 'pi_flexform')
+            ->from('tt_content')
             ->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)
+                    'CType',
+                    $queryBuilder->createNamedParameter('form_formframework', \PDO::PARAM_STR)
                 )
             )
             ->execute()
@@ -324,66 +723,35 @@ class FormFileExtensionUpdate extends AbstractUpdate
     }
 
     /**
-     * @param array $referencedFormFiles
-     * @return array
+     * @param int $uid
+     * @param string $flexform
+     * @param bool $updateRefindex
      */
-    protected function groupReferencedExtensionFormFiles(
-        array $referencedFormFiles
-    ): array {
-        $referencedExtensionFormFiles = [];
+    protected function updateContentReference(
+        int $uid,
+        string $flexform,
+        bool $updateRefindex = false
+    ) {
+        $this->connection->update(
+            'tt_content',
+            ['pi_flexform' => $flexform],
+            ['uid' => $uid]
+        );
 
-        foreach ($referencedFormFiles as $referencedFormFile) {
-            $identifier = $referencedFormFile['identifier'];
-            if ((int)$referencedFormFile['storage'] !== 0
-                || strpos($identifier, '/typo3conf/ext/') !== 0
-            ) {
-                continue;
-            }
-            $referencedExtensionFormFiles[$identifier][] = $referencedFormFile;
+        if (!$updateRefindex) {
+            return;
         }
 
-        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()
+        $this->referenceIndex->updateRefIndexTable(
+            'tt_content',
+            $uid
         );
     }
 
     /**
      * @return ObjectManager
      */
-    protected function getObjectManager()
+    protected function getObjectManager(): ObjectManager
     {
         return GeneralUtility::makeInstance(ObjectManager::class);
     }
index b5f7ba3..63551b3 100644 (file)
@@ -297,39 +297,33 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
             }
         }
 
-        foreach ($this->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
-            $relativePath = rtrim($relativePath, '/') . '/';
-            foreach (new \DirectoryIterator($fullPath) as $fileInfo) {
-                if ($fileInfo->getExtension() !== 'yaml') {
-                    continue;
-                }
-                $form = $this->load($relativePath . $fileInfo->getFilename());
-                if (isset($form['identifier'], $form['type']) && $form['type'] === 'Form') {
-                    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,
-                        ];
-                    }
+        foreach ($this->retrieveYamlFilesFromExtensionFolders() as $fullPath => $fileName) {
+            $form = $this->load($fullPath);
+            if (isset($form['identifier'], $form['type']) && $form['type'] === 'Form') {
+                if ($this->hasValidFileExtension($fileName)) {
+                    $forms[] = [
+                        'identifier' => $form['identifier'],
+                        'name' => $form['label'] ?? $form['identifier'],
+                        'persistenceIdentifier' => $fullPath,
+                        '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' => $fullPath,
+                        'readOnly' => true,
+                        'removable' => false,
+                        'location' => 'extension',
+                        'duplicateIdentifier' => false,
+                        'invalid' => false,
+                        'deprecatedFileExtension' => true,
+                    ];
                 }
             }
         }
@@ -382,6 +376,29 @@ class FormPersistenceManager implements FormPersistenceManagerInterface
     }
 
     /**
+     * Retrieves yaml files from extension folders for further processing.
+     * At this time it's not determined yet, whether these files contain form data.
+     *
+     * @return File[]
+     * @internal
+     */
+    public function retrieveYamlFilesFromExtensionFolders(): array
+    {
+        $filesFromExtensionFolders = [];
+
+        foreach ($this->getAccessibleExtensionFolders() as $relativePath => $fullPath) {
+            foreach (new \DirectoryIterator($fullPath) as $fileInfo) {
+                if ($fileInfo->getExtension() !== 'yaml') {
+                    continue;
+                }
+                $filesFromExtensionFolders[$relativePath . $fileInfo->getFilename()] = $fileInfo->getFilename();
+            }
+        }
+
+        return $filesFromExtensionFolders;
+    }
+
+    /**
      * Return a list of all accessible file mountpoints for the
      * current backend user.
      *
diff --git a/typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Form/legacy.yaml b/typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Form/legacy.yaml
new file mode 100644 (file)
index 0000000..a7e46df
--- /dev/null
@@ -0,0 +1,3 @@
+type: Form
+identifier: legacy
+prototypeName: standard
diff --git a/typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Form/updated.form.yaml b/typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Form/updated.form.yaml
new file mode 100644 (file)
index 0000000..345eb3b
--- /dev/null
@@ -0,0 +1,3 @@
+type: Form
+identifier: updated
+prototypeName: standard
diff --git a/typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Yaml/AllowedExtensionPaths.yaml b/typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/Configuration/Yaml/AllowedExtensionPaths.yaml
new file mode 100644 (file)
index 0000000..9bb889e
--- /dev/null
@@ -0,0 +1,6 @@
+TYPO3:
+    CMS:
+        Form:
+            persistenceManager:
+                allowedExtensionPaths:
+                    110: EXT:test_resources/Configuration/Form/
diff --git a/typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/ext_emconf.php b/typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources/ext_emconf.php
new file mode 100644 (file)
index 0000000..c606d86
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+$EM_CONF[$_EXTKEY] = [
+    'title' => 'Test Resources',
+    'description' => 'Test Resources',
+    'category' => 'example',
+    'version' => '9.3.3',
+    'state' => 'beta',
+    'uploadfolder' => 0,
+    'createDirs' => '',
+    'clearCacheOnLoad' => 0,
+    'author' => 'Oliver Hader',
+    'author_email' => 'oliver@typo3.org',
+    'author_company' => '',
+    'constraints' => [
+        'depends' => [
+            'typo3' => '9.3.3'
+        ],
+        'conflicts' => [],
+        'suggests' => [],
+    ],
+];
diff --git a/typo3/sysext/form/Tests/Functional/Hooks/FormFileExtensionUpdateTest.php b/typo3/sysext/form/Tests/Functional/Hooks/FormFileExtensionUpdateTest.php
new file mode 100644 (file)
index 0000000..ce63efa
--- /dev/null
@@ -0,0 +1,586 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Form\Tests\Functional\Hooks;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
+use TYPO3\CMS\Core\Core\Bootstrap;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Database\ReferenceIndex;
+use TYPO3\CMS\Core\Resource\Folder;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
+use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\StringUtility;
+use TYPO3\CMS\Form\Hooks\FormFileExtensionUpdate;
+use TYPO3\CMS\Form\Slot\FilePersistenceSlot;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+class FormFileExtensionUpdateTest extends FunctionalTestCase
+{
+    /**
+     * @var string[]
+     */
+    protected $coreExtensionsToLoad = [
+        'form',
+    ];
+
+    /**
+     * @var string[]
+     */
+    protected $testExtensionsToLoad = [
+        'typo3/sysext/form/Tests/Functional/Hooks/Fixtures/test_resources',
+    ];
+
+    /**
+     * @var FormFileExtensionUpdate
+     */
+    private $subject;
+
+    /**
+     * @var FilePersistenceSlot
+     */
+    private $slot;
+
+    /**
+     * @var FlexFormTools
+     */
+    private $flexForm;
+
+    /**
+     * @var ReferenceIndex
+     */
+    private $referenceIndex;
+
+    /**
+     * @var Folder
+     */
+    private $storageFolder;
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->setUpBackendUserFromFixture(1);
+        Bootstrap::getInstance()->initializeLanguageObject();
+
+        $folderIdentifier = 'user_upload';
+        $storage = ResourceFactory::getInstance()->getStorageObject(1);
+
+        if ($storage->hasFolder($folderIdentifier)) {
+            $storage->getFolder($folderIdentifier)->delete(true);
+        }
+
+        $this->subject = GeneralUtility::makeInstance(FormFileExtensionUpdate::class);
+        $this->slot = GeneralUtility::makeInstance(FilePersistenceSlot::class);
+        $this->flexForm = GeneralUtility::makeInstance(FlexFormTools::class);
+        $this->referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
+        $this->storageFolder = $storage->createFolder($folderIdentifier);
+    }
+
+    protected function tearDown()
+    {
+        $this->storageFolder->delete(true);
+        parent::tearDown();
+    }
+
+    /*
+     * --- CHECK FOR UPDATE ---
+     */
+
+    /**
+     * @return bool
+     */
+    private function invokeCheckForUpdate(): bool
+    {
+        $description = '';
+        return $this->subject->checkForUpdate($description);
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsNotRequiredHavingUpdatedFormDefinitions()
+    {
+        $this->createStorageFormDefinition('updated', false);
+        $this->assertFalse($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsRequiredHavingOutdatedStorageFormDefinitions()
+    {
+        $this->createStorageFormDefinition('legacy', true);
+        $this->assertTrue($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsNotRequiredHavingUpdatedStorageReferences()
+    {
+        $this->createStorageFormDefinition('updated', false);
+        $this->createReference(
+            $this->createStorageFileIdentifier('updated.form.yaml'),
+            'updated'
+        );
+        $this->assertFalse($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsNotRequiredHavingUpdatedStorageReferencesWithFinisherOverrides(
+    ) {
+        $this->createStorageFormDefinition('updated', false);
+        $finisherOverrides = [
+            'FirstFinisher' => StringUtility::getUniqueId(),
+            'SecondFinisher' => StringUtility::getUniqueId(),
+        ];
+        $this->createReference(
+            $this->createStorageFileIdentifier('updated.form.yaml'),
+            'updated',
+            $finisherOverrides
+        );
+        $this->assertFalse($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsRequiredHavingOutdatedStorageReferences()
+    {
+        // form definition was renamed already
+        $this->createStorageFormDefinition('updated', false);
+        // but references not updated yet
+        $this->createReference(
+            $this->createStorageFileIdentifier('updated.yaml'),
+            'updated'
+        );
+        $this->assertTrue($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsRequiredHavingOutdatedStorageReferencesWithFinisherOverrides(
+    ) {
+        // form definition was renamed already
+        $this->createStorageFormDefinition('updated', false);
+        // but references not updated yet
+        $finisherOverrides = [
+            'FirstFinisher' => StringUtility::getUniqueId(),
+            'SecondFinisher' => StringUtility::getUniqueId(),
+        ];
+        $this->createReference(
+            $this->createStorageFileIdentifier('updated.yaml'),
+            'updated',
+            $finisherOverrides
+        );
+        $this->assertTrue($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsNotRequiredHavingOutdatedExtensionFormDefinitions()
+    {
+        $this->setUpAllowedExtensionPaths();
+        $this->assertFalse($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsNotRequiredHavingUpdatedExtensionReferences()
+    {
+        $this->setUpAllowedExtensionPaths();
+        $this->createReference(
+            $this->createExtensionFileIdentifier('updated.form.yaml'),
+            'updated'
+        );
+        $this->assertFalse($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsRequiredHavingOutdatedExtensionReferences()
+    {
+        $this->setUpAllowedExtensionPaths();
+        $this->createReference(
+            $this->createExtensionFileIdentifier('updated.yaml'),
+            'updated'
+        );
+        $this->assertTrue($this->invokeCheckForUpdate());
+    }
+
+    /**
+     * @test
+     */
+    public function updateIsRequiredHavingOutdatedExtensionReferencesWithFinisherOverrides(
+    ) {
+        $this->setUpAllowedExtensionPaths();
+        $finisherOverrides = [
+            'FirstFinisher' => StringUtility::getUniqueId(),
+            'SecondFinisher' => StringUtility::getUniqueId(),
+        ];
+        $this->createReference(
+            $this->createExtensionFileIdentifier('updated.yaml'),
+            'updated',
+            $finisherOverrides
+        );
+        $this->assertTrue($this->invokeCheckForUpdate());
+    }
+
+    /*
+     * --- PERFORM UPDATE ---
+     */
+
+    private function invokePerformUpdate(): bool
+    {
+        $queries = [];
+        $messages = '';
+        return $this->subject->performUpdate(
+            $queries,
+            $messages
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function performUpdateSucceedsHavingOutdatedStorageFormDefinitions()
+    {
+        $this->createStorageFormDefinition('legacy', true);
+        $this->assertTrue(
+            $this->invokePerformUpdate()
+        );
+        $this->assertTrue(
+            $this->storageFolder->hasFile('legacy.form.yaml')
+        );
+    }
+
+    /**
+     * @test
+     */
+    public function performUpdateSucceedsHavingOutdatedStorageReferences()
+    {
+        // form definition was renamed already
+        $this->createStorageFormDefinition('updated', false);
+        // but references not updated yet
+        $this->createReference(
+            $this->createStorageFileIdentifier('updated.yaml'),
+            'updated'
+        );
+        // having an additional reference
+        $this->createReference(
+            $this->createStorageFileIdentifier('updated.yaml'),
+            'updated'
+        );
+        $this->assertTrue(
+            $this->invokePerformUpdate()
+        );
+        $expectedFileIdentifier = $this->createStorageFileIdentifier(
+            'updated.form.yaml'
+        );
+        foreach ($this->retrieveAllFlexForms() as $flexForm) {
+            $this->assertSame(
+                $expectedFileIdentifier,
+                $flexForm['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF']
+            );
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function performUpdateSucceedsHavingOutdatedStorageReferencesWithFinisherOverrides(
+    ) {
+        // form definition was renamed already
+        $this->createStorageFormDefinition('updated', false);
+        // but references not updated yet
+        $finisherOverrides = [
+            'FirstFinisher' => StringUtility::getUniqueId(),
+            'SecondFinisher' => StringUtility::getUniqueId(),
+        ];
+        $this->createReference(
+            $this->createStorageFileIdentifier('updated.yaml'),
+            'updated',
+            $finisherOverrides
+        );
+        // having an additional reference
+        $this->createReference(
+            $this->createStorageFileIdentifier('updated.yaml'),
+            'updated',
+            $finisherOverrides
+        );
+        $this->assertTrue(
+            $this->invokePerformUpdate()
+        );
+        $expectedFileIdentifier = $this->createStorageFileIdentifier(
+            'updated.form.yaml'
+        );
+        $expectedSheetIdentifiers = $this->createFinisherOverridesSheetIdentifiers(
+            $expectedFileIdentifier,
+            'updated',
+            $finisherOverrides
+        );
+        foreach ($this->retrieveAllFlexForms() as $flexForm) {
+            $this->assertSame(
+                $expectedFileIdentifier,
+                $flexForm['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF'] ?? null
+            );
+            foreach ($finisherOverrides as $finisherIdentifier => $finisherValue) {
+                $sheetIdentifier = $expectedSheetIdentifiers[$finisherIdentifier];
+                $propertyName = sprintf(
+                    'settings.finishers.%s.value',
+                    $finisherIdentifier
+                );
+                $this->assertSame(
+                    $finisherValue,
+                    $flexForm['data'][$sheetIdentifier]['lDEF'][$propertyName]['vDEF'] ?? null
+                );
+            }
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function performUpdateSucceedsHavingOutdatedExtensionReferences()
+    {
+        $this->setUpAllowedExtensionPaths();
+        $this->createReference(
+            $this->createExtensionFileIdentifier('updated.yaml'),
+            'updated'
+        );
+        // having an additional reference
+        $this->createReference(
+            $this->createExtensionFileIdentifier('updated.yaml'),
+            'updated'
+        );
+        $this->assertTrue(
+            $this->invokePerformUpdate()
+        );
+        $expectedFileIdentifier = $this->createExtensionFileIdentifier(
+            'updated.form.yaml'
+        );
+        foreach ($this->retrieveAllFlexForms() as $flexForm) {
+            $this->assertSame(
+                $expectedFileIdentifier,
+                $flexForm['data']['sDEF']['lDEF']['settings.persistenceIdentifier']['vDEF'] ?? null
+            );
+        }
+    }
+
+    /**
+     * @test
+     */
+    public function performUpdateSucceedsHavingOutdatedExtensionReferencesWithFinisherOverrides(
+    ) {
+        $this->setUpAllowedExtensionPaths();
+        $finisherOverrides = [
+            'FirstFinisher' => StringUtility::getUniqueId(),
+            'SecondFinisher' => StringUtility::getUniqueId(),
+        ];
+        $this->createReference(
+            $this->createExtensionFileIdentifier('updated.yaml'),
+            'updated',
+            $finisherOverrides
+        );
+        // having an additional reference
+        $this->createReference(
+            $this->createExtensionFileIdentifier('updated.yaml'),
+            'updated',
+            $finisherOverrides
+        );
+        $this->assertTrue(
+            $this->invokePerformUpdate()
+        );
+    }
+
+    /*
+     * --- HELPER FUNCTIONS ---
+     */
+
+    /**
+     * @param string $name
+     * @param bool $legacy
+     */
+    private function createStorageFormDefinition(
+        string $name,
+        bool $legacy = false
+    ) {
+        $content = implode(LF, [
+            'type: Form',
+            'identifier: ' . $name,
+            'prototypeName: standard'
+        ]);
+
+        $fileName = $name . '.' . ($legacy ? 'yaml' : 'form.yaml');
+        $fileIdentifier = $this->createStorageFileIdentifier($fileName);
+
+        if (!$legacy) {
+            $this->slot->allowInvocation(
+                FilePersistenceSlot::COMMAND_FILE_CREATE,
+                $fileIdentifier
+            );
+            $this->slot->allowInvocation(
+                FilePersistenceSlot::COMMAND_FILE_SET_CONTENTS,
+                $fileIdentifier,
+                $this->slot->getContentSignature($content)
+            );
+        }
+
+        $this->storageFolder->createFile($fileName)->setContents($content);
+    }
+
+    /**
+     * @param string $fileIdentifier
+     * @param string $formIdentifier
+     * @param array $finisherOverrides
+     */
+    private function createReference(
+        string $fileIdentifier,
+        string $formIdentifier,
+        array $finisherOverrides = []
+    ) {
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $connection = $connectionPool->getConnectionForTable('tt_content');
+
+        $flexForm = [
+            'data' => [
+                'sDEF' => [
+                    'lDEF' => [
+                        'settings.persistenceIdentifier' => [
+                            'vDEF' => $fileIdentifier,
+                        ],
+                        'settings.overrideFinishers' => [
+                            'vDEF' => empty($finisherOverrides) ? '0' : '1',
+                        ],
+                    ]
+                ]
+            ]
+        ];
+
+        $sheetIdentifiers = $this->createFinisherOverridesSheetIdentifiers(
+            $fileIdentifier,
+            $formIdentifier,
+            $finisherOverrides
+        );
+        foreach ($finisherOverrides as $finisherIdentifier => $finisherValue) {
+            $sheetIdentifier = $sheetIdentifiers[$finisherIdentifier];
+            $propertyName = sprintf(
+                'settings.finishers.%s.value',
+                $finisherIdentifier
+            );
+            $flexForm['data'][$sheetIdentifier]['lDEF'] = [
+                $propertyName => [
+                    'vDEF' => $finisherValue
+                ],
+            ];
+        }
+
+        $values = [
+            'pid' => 1,
+            'header' => sprintf(
+                'Form Content Element for "%s"',
+                $formIdentifier
+            ),
+            'CType' => 'form_formframework',
+            'pi_flexform' => $this->flexForm
+                ->flexArray2Xml($flexForm, true)
+        ];
+
+        $connection->insert('tt_content', $values);
+        $id = $connection->lastInsertId('tt_content');
+        $this->referenceIndex->updateRefIndexTable('tt_content', $id);
+    }
+
+    /**
+     * Sets up additional paths to allow using form definitions from extension.
+     */
+    private function setUpAllowedExtensionPaths()
+    {
+        ExtensionManagementUtility::addTypoScriptSetup(trim('
+            module.tx_form.settings.yamlConfigurations {
+                110 = EXT:test_resources/Configuration/Yaml/AllowedExtensionPaths.yaml
+            }
+            plugin.tx_form.settings.yamlConfigurations {
+                110 = EXT:test_resources/Configuration/Yaml/AllowedExtensionPaths.yaml
+            }
+        '));
+    }
+
+    /**
+     * @return array
+     */
+    private function retrieveAllFlexForms(): array
+    {
+        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+        $connection = $connectionPool->getConnectionForTable('tt_content');
+
+        return array_map(
+            function (array $record) {
+                return GeneralUtility::xml2array($record['pi_flexform']);
+            },
+            $connection->select(['pi_flexform'], 'tt_content')
+                ->fetchAll(\PDO::FETCH_ASSOC)
+        );
+    }
+
+    /**
+     * @param string $fileIdentifier
+     * @param string $formIdentifier
+     * @param array $finisherOverrides
+     * @return array
+     */
+    private function createFinisherOverridesSheetIdentifiers(
+        string $fileIdentifier,
+        string $formIdentifier,
+        array $finisherOverrides
+    ): array {
+        $sheetIdentifiers = [];
+        foreach (array_keys($finisherOverrides) as $finisherIdentifier) {
+            $sheetIdentifiers[$finisherIdentifier] = md5(
+                $fileIdentifier
+                . 'standard'
+                . $formIdentifier
+                . $finisherIdentifier
+            );
+        }
+        return $sheetIdentifiers;
+    }
+
+    /**
+     * @param string $fileName
+     * @return string
+     */
+    private function createStorageFileIdentifier(string $fileName): string
+    {
+        return $this->storageFolder->getCombinedIdentifier() . $fileName;
+    }
+
+    /**
+     * @param string $fileName
+     * @return string
+     */
+    private function createExtensionFileIdentifier(string $fileName): string
+    {
+        return 'EXT:test_resources/Configuration/Form/' . $fileName;
+    }
+}