diff --git a/Classes/Updates/v104/DatabaseRowsUpdateWizard.php b/Classes/Updates/v104/DatabaseRowsUpdateWizard.php
new file mode 100644
index 0000000..84cc782
--- /dev/null
+++ b/Classes/Updates/v104/DatabaseRowsUpdateWizard.php
@@ -0,0 +1,363 @@
+rowUpdater;
+ }
+
+ /**
+ * @return string Unique identifier of this updater
+ */
+ public function getIdentifier(): string
+ {
+ return 'databaseRowsUpdateWizard';
+ }
+
+ /**
+ * @return string Title of this updater
+ */
+ public function getTitle(): string
+ {
+ return 'Execute database migrations on single rows';
+ }
+
+ /**
+ * @return string Longer description of this updater
+ * @throws \RuntimeException
+ */
+ public function getDescription(): string
+ {
+ $rowUpdaterNotExecuted = $this->getRowUpdatersToExecute();
+ $description = 'Row updaters that have not been executed:';
+ foreach ($rowUpdaterNotExecuted as $rowUpdateClassName) {
+ $rowUpdater = GeneralUtility::makeInstance($rowUpdateClassName);
+ if (!$rowUpdater instanceof RowUpdaterInterface) {
+ throw new \RuntimeException(
+ 'Row updater must implement RowUpdaterInterface',
+ 1484066647
+ );
+ }
+ $description .= LF . $rowUpdater->getTitle();
+ }
+ return $description;
+ }
+
+ /**
+ * @return bool True if at least one row updater is not marked done
+ */
+ public function updateNecessary(): bool
+ {
+ return !empty($this->getRowUpdatersToExecute());
+ }
+
+ /**
+ * @return string[] All new fields and tables must exist
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * Performs the configuration update.
+ *
+ * @return bool
+ * @throws \Doctrine\DBAL\ConnectionException
+ * @throws \Exception
+ */
+ public function executeUpdate(): bool
+ {
+ $registry = GeneralUtility::makeInstance(Registry::class);
+
+ // If rows from the target table that is updated and the sys_registry table are on the
+ // same connection, the row update statement and sys_registry position update will be
+ // handled in a transaction to have an atomic operation in case of errors during execution.
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
+
+ /** @var RowUpdaterInterface[] $rowUpdaterInstances */
+ $rowUpdaterInstances = [];
+ // Single row updater instances are created only once for this method giving
+ // them a chance to set up local properties during hasPotentialUpdateForTable()
+ // and using that in updateTableRow()
+ foreach ($this->getRowUpdatersToExecute() as $rowUpdater) {
+ $rowUpdaterInstance = GeneralUtility::makeInstance($rowUpdater);
+ if (!$rowUpdaterInstance instanceof RowUpdaterInterface) {
+ throw new \RuntimeException(
+ 'Row updater must implement RowUpdaterInterface',
+ 1484071612
+ );
+ }
+ $rowUpdaterInstances[] = $rowUpdaterInstance;
+ }
+
+ // Scope of the row updater is to update all rows that have TCA,
+ // our list of tables is just the list of loaded TCA tables.
+ /** @var string[] $listOfAllTables */
+ $listOfAllTables = array_keys($GLOBALS['TCA']);
+
+ // In case the PHP ended for whatever reason, fetch the last position from registry
+ // and throw away all tables before that start point.
+ sort($listOfAllTables);
+ reset($listOfAllTables);
+ $firstTable = current($listOfAllTables);
+ $startPosition = $this->getStartPosition($firstTable);
+ foreach ($listOfAllTables as $key => $table) {
+ if ($table === $startPosition['table']) {
+ break;
+ }
+ unset($listOfAllTables[$key]);
+ }
+
+ // Ask each row updater if it potentially has field updates for rows of a table
+ $tableToUpdaterList = [];
+ foreach ($listOfAllTables as $table) {
+ foreach ($rowUpdaterInstances as $updater) {
+ if ($updater->hasPotentialUpdateForTable($table)) {
+ if (!is_array($tableToUpdaterList[$table])) {
+ $tableToUpdaterList[$table] = [];
+ }
+ $tableToUpdaterList[$table][] = $updater;
+ }
+ }
+ }
+
+ // Iterate through all rows of all tables that have potential row updaters attached,
+ // feed each single row to each updater and finally update each row in database if
+ // a row updater changed a fields
+ foreach ($tableToUpdaterList as $table => $updaters) {
+ /** @var RowUpdaterInterface[] $updaters */
+ $connectionForTable = $connectionPool->getConnectionForTable($table);
+ $queryBuilder = $connectionPool->getQueryBuilderForTable($table);
+ $queryBuilder->getRestrictions()->removeAll();
+ $queryBuilder->select('*')
+ ->from($table)
+ ->orderBy('uid');
+ if ($table === $startPosition['table']) {
+ $queryBuilder->where(
+ $queryBuilder->expr()->gt('uid', $queryBuilder->createNamedParameter($startPosition['uid']))
+ );
+ }
+ $statement = $queryBuilder->execute();
+ $rowCountWithoutUpdate = 0;
+ while ($row = $rowBefore = $statement->fetch()) {
+ foreach ($updaters as $updater) {
+ $row = $updater->updateTableRow($table, $row);
+ }
+ $updatedFields = array_diff_assoc($row, $rowBefore);
+ if (empty($updatedFields)) {
+ // Updaters changed no field of that row
+ $rowCountWithoutUpdate++;
+ if ($rowCountWithoutUpdate >= 200) {
+ // Update startPosition if there were many rows without data change
+ $startPosition = [
+ 'table' => $table,
+ 'uid' => $row['uid'],
+ ];
+ $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
+ $rowCountWithoutUpdate = 0;
+ }
+ } else {
+ $rowCountWithoutUpdate = 0;
+ $startPosition = [
+ 'table' => $table,
+ 'uid' => $rowBefore['uid'],
+ ];
+ if ($connectionForSysRegistry === $connectionForTable
+ && !($connectionForSysRegistry->getDatabasePlatform() instanceof SQLServerPlatform)
+ ) {
+ // Target table and sys_registry table are on the same connection and not mssql, use a transaction
+ $connectionForTable->beginTransaction();
+ try {
+ $this->updateOrDeleteRow(
+ $connectionForTable,
+ $connectionForTable,
+ $table,
+ (int)$rowBefore['uid'],
+ $updatedFields,
+ $startPosition
+ );
+ $connectionForTable->commit();
+ } catch (\Exception $up) {
+ $connectionForTable->rollBack();
+ throw $up;
+ }
+ } else {
+ // Either different connections for table and sys_registry, or mssql.
+ // SqlServer can not run a transaction for a table if the same table is queried
+ // currently - our above ->fetch() main loop.
+ // So, execute two distinct queries and hope for the best.
+ $this->updateOrDeleteRow(
+ $connectionForTable,
+ $connectionForSysRegistry,
+ $table,
+ (int)$rowBefore['uid'],
+ $updatedFields,
+ $startPosition
+ );
+ }
+ }
+ }
+ }
+
+ // Ready with updates, remove position information from sys_registry
+ $registry->remove('installUpdateRows', 'rowUpdatePosition');
+ // Mark row updaters that were executed as done
+ foreach ($rowUpdaterInstances as $updater) {
+ $this->setRowUpdaterExecuted($updater);
+ }
+
+ return true;
+ }
+
+ /**
+ * Return an array of class names that are not yet marked as done.
+ *
+ * @return array Class names
+ */
+ protected function getRowUpdatersToExecute(): array
+ {
+ $doneRowUpdater = GeneralUtility::makeInstance(Registry::class)->get('installUpdateRows', 'rowUpdatersDone', []);
+ return array_diff($this->rowUpdater, $doneRowUpdater);
+ }
+
+ /**
+ * Mark a single updater as done
+ *
+ * @param RowUpdaterInterface $updater
+ */
+ protected function setRowUpdaterExecuted(RowUpdaterInterface $updater)
+ {
+ $registry = GeneralUtility::makeInstance(Registry::class);
+ $doneRowUpdater = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
+ $doneRowUpdater[] = get_class($updater);
+ $registry->set('installUpdateRows', 'rowUpdatersDone', $doneRowUpdater);
+ }
+
+ /**
+ * Return an array with table / uid combination that specifies the start position the
+ * update row process should start with.
+ *
+ * @param string $firstTable Table name of the first TCA in case the start position needs to be initialized
+ * @return array New start position
+ */
+ protected function getStartPosition(string $firstTable): array
+ {
+ $registry = GeneralUtility::makeInstance(Registry::class);
+ $startPosition = $registry->get('installUpdateRows', 'rowUpdatePosition', []);
+ if (empty($startPosition)) {
+ $startPosition = [
+ 'table' => $firstTable,
+ 'uid' => 0,
+ ];
+ $registry->set('installUpdateRows', 'rowUpdatePosition', $startPosition);
+ }
+ return $startPosition;
+ }
+
+ /**
+ * @param Connection $connectionForTable
+ * @param string $table
+ * @param array $updatedFields
+ * @param int $uid
+ * @param Connection $connectionForSysRegistry
+ * @param array $startPosition
+ */
+ protected function updateOrDeleteRow(Connection $connectionForTable, Connection $connectionForSysRegistry, string $table, int $uid, array $updatedFields, array $startPosition): void
+ {
+ $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
+ if ($deleteField === null && $updatedFields['deleted'] === 1) {
+ $connectionForTable->delete(
+ $table,
+ [
+ 'uid' => $uid,
+ ]
+ );
+ } else {
+ $connectionForTable->update(
+ $table,
+ $updatedFields,
+ [
+ 'uid' => $uid,
+ ]
+ );
+ }
+ $connectionForSysRegistry->update(
+ 'sys_registry',
+ [
+ 'entry_value' => serialize($startPosition),
+ ],
+ [
+ 'entry_namespace' => 'installUpdateRows',
+ 'entry_key' => 'rowUpdatePosition',
+ ],
+ [
+ // Needs to be declared LOB, so MSSQL can handle the conversion from string (nvarchar) to blob (varbinary)
+ 'entry_value' => \PDO::PARAM_LOB,
+ 'entry_namespace' => \PDO::PARAM_STR,
+ 'entry_key' => \PDO::PARAM_STR,
+ ]
+ );
+ }
+}
diff --git a/Classes/Updates/v104/FeeditExtractionUpdate.php b/Classes/Updates/v104/FeeditExtractionUpdate.php
new file mode 100644
index 0000000..91a05a3
--- /dev/null
+++ b/Classes/Updates/v104/FeeditExtractionUpdate.php
@@ -0,0 +1,122 @@
+extension = new ExtensionModel(
+ 'feedit',
+ 'Deprecated feedit extension',
+ '10.0.0',
+ 'friendsoftypo3/feedit',
+ 'Contains an old approach for content editing in a TYPO3 Frontend.'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'This extension is outdated and does not work very well. ' . $this->extension->getDescription(),
+ false
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'feeditExtension';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install outdated extension "feedit" from TER if editors used this extension in earlier core versions.';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The extension "feedit" allows editing content elements directly in the Frontend.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !ExtensionManagementUtility::isLoaded('feedit');
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+}
diff --git a/Classes/Updates/v104/FormFileExtensionUpdate.php b/Classes/Updates/v104/FormFileExtensionUpdate.php
new file mode 100644
index 0000000..64a14a9
--- /dev/null
+++ b/Classes/Updates/v104/FormFileExtensionUpdate.php
@@ -0,0 +1,836 @@
+output = $output;
+ }
+
+ /**
+ * Checks whether updates are required.
+ *
+ * @return bool Whether an update is required (TRUE) or not (FALSE)
+ */
+ public function updateNecessary(): bool
+ {
+ $updateNeeded = false;
+
+ $this->persistenceManager = $this->getObjectManager()->get(FormPersistenceManager::class);
+ $this->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+
+ 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;
+ $this->output->writeln('Form definition files were found that should be migrated to be named .form.yaml.');
+ }
+
+ if (
+ $formDefinitionInformation['hasNewFileExtension']
+ && $formDefinitionInformation['hasReferencesForOldFileExtension']
+ ) {
+ $updateNeeded = true;
+ $this->output->writeln('Referenced form definition files found that should be updated.');
+ }
+
+ if (
+ $formDefinitionInformation['referencesForOldFileExtensionNeedsFlexformUpdates'] === true
+ || $formDefinitionInformation['referencesForNewFileExtensionNeedsFlexformUpdates'] === true
+ ) {
+ $updateNeeded = true;
+ if ($formDefinitionInformation['hasNewFileExtension'] === true) {
+ $this->output->writeln('Referenced form definition files found that should be updated.');
+ } elseif ($formDefinitionInformation['location'] === 'storage') {
+ $this->output->writeln('Referenced form definition files found that should be updated.');
+ } else {
+ $this->output->writeln(
+ '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.'
+ );
+ }
+ }
+ }
+
+ return $updateNeeded;
+ }
+
+ /**
+ * Performs the accordant updates.
+ *
+ * @return bool Whether everything went smoothly or not
+ */
+ public function executeUpdate(): bool
+ {
+ $success = true;
+
+ $GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::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->resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ $this->referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
+ $this->flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
+
+ $filePersistenceSlot->defineInvocation(
+ FilePersistenceSlot::COMMAND_FILE_RENAME,
+ true
+ );
+
+ $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;
+ }
+
+ 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;
+ }
+
+ if ($formDefinitionInformation['location'] === 'storage') {
+ $file = $formDefinitionInformation['file'];
+
+ $newPossiblePersistenceIdentifier = $this->persistenceManager->getUniquePersistenceIdentifier(
+ $file->getNameWithoutExtension(),
+ $file->getParentFolder()->getCombinedIdentifier()
+ );
+ $newFileName = PathUtility::pathinfo(
+ $newPossiblePersistenceIdentifier,
+ PATHINFO_BASENAME
+ );
+ $newFileName = is_string($newFileName) ? $newFileName : '';
+
+ try {
+ $file->rename($newFileName, DuplicationBehavior::RENAME);
+ $newPersistenceIdentifier = $file->getCombinedIdentifier();
+ } catch (\Exception $e) {
+ $this->output->writeln(sprintf(
+ 'Failed to rename form definition "%s" to "%s".',
+ $formDefinitionInformation['persistenceIdentifier'],
+ $newFileName
+ ));
+ $success = false;
+ 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 {
+ $success = false;
+ $this->output->writeln(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'])
+ ));
+ }
+ }
+
+ $filePersistenceSlot->defineInvocation(
+ FilePersistenceSlot::COMMAND_FILE_RENAME,
+ null
+ );
+
+ return $success;
+ }
+
+ /**
+ * @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;
+ }
+
+ $formDefinitionsInformation[$persistenceIdentifier] = $this->setFormDefinitionInformationData(
+ $persistenceIdentifier,
+ $formDefinition,
+ $file,
+ 'storage'
+ );
+ }
+
+ return $formDefinitionsInformation;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getFormDefinitionsInformationFromExtensions(): array
+ {
+ $formDefinitionsInformation = [];
+
+ foreach ($this->persistenceManager->retrieveYamlFilesFromExtensionFolders() as $persistenceIdentifier => $_) {
+ try {
+ /** @var File $file */
+ $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'
+ );
+ }
+
+ return $formDefinitionsInformation;
+ }
+
+ /**
+ * @param string $persistenceIdentifier
+ * @param array $formDefinition
+ * @param File $file
+ * @param string $location
+ * @return array
+ */
+ protected function setFormDefinitionInformationData(
+ string $persistenceIdentifier,
+ array $formDefinition,
+ File $file,
+ string $location
+ ): array {
+ return [
+ 'location' => $location,
+ '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']);
+ if (!is_array($flexform)) {
+ // * There is no data other than a base XML-structure in pi_flexform:
+ // xml2array returns empty string or newline-character (string)
+ // * pi_flexform is invalid XML:
+ // xml2array returns an error message (string)
+ continue;
+ }
+ $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';
+ }
+ } 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']);
+ }
+ }
+
+ 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 $sheetIdentifiersWhichNeedsUpdate;
+ }
+
+ /**
+ * @param array $flexform
+ * @return array
+ */
+ protected function getSheetIdentifiersForFinisherOverrides(array $flexform): array
+ {
+ $sheetIdentifiers = [];
+ foreach ($this->getFinisherSheetsFromFlexform($flexform) as $sheetIdentifier => $sheetData) {
+ $itemOptionPath = array_keys($sheetData['lDEF']);
+ $firstSheetItemOptionPath = (string)array_shift($itemOptionPath);
+ preg_match('#^settings\.finishers\.(.*)\..+$#', $firstSheetItemOptionPath, $matches);
+ if (!isset($matches[1])) {
+ continue;
+ }
+ $sheetIdentifiers[$sheetIdentifier] = $matches[1];
+ }
+
+ return $sheetIdentifiers;
+ }
+
+ /**
+ * @param array $flexform
+ * @return array
+ */
+ protected function getFinisherSheetsFromFlexform(array $flexform): array
+ {
+ if (!isset($flexform['data'])) {
+ return [];
+ }
+
+ return array_filter(
+ $flexform['data'],
+ function ($key) {
+ return $key !== 'sDEF' && strlen($key) === 32;
+ },
+ ARRAY_FILTER_USE_KEY
+ );
+ }
+
+ /**
+ * @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
+ );
+ }
+
+ /**
+ * @param array $formDefinition
+ * @return bool
+ */
+ protected function looksLikeAFormDefinition(array $formDefinition): bool
+ {
+ return isset($formDefinition['identifier'], $formDefinition['type']) && $formDefinition['type'] === 'Form';
+ }
+
+ /**
+ * @param string $persistenceIdentifier
+ * @return 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(
+ '#(?getContents();
+ $formDefinition = $this->extractMetaDataFromCouldBeFormDefinition($rawYamlContent);
+
+ if (!$this->looksLikeAFormDefinition($formDefinition)) {
+ $formDefinition = [];
+ }
+ } catch (\Exception $exception) {
+ $formDefinition = [];
+ }
+
+ return $formDefinition;
+ }
+
+ /**
+ * @param string $maybeRawFormDefinition
+ * @return array
+ */
+ protected function extractMetaDataFromCouldBeFormDefinition(string $maybeRawFormDefinition): array
+ {
+ $metaDataProperties = ['identifier', 'type', 'label', 'prototypeName'];
+ $metaData = [];
+ foreach (explode("\n", $maybeRawFormDefinition) as $line) {
+ if (empty($line) || $line[0] === ' ') {
+ continue;
+ }
+
+ [$key, $value] = explode(':', $line);
+ if (
+ empty($key)
+ || empty($value)
+ || !in_array($key, $metaDataProperties)
+ ) {
+ continue;
+ }
+
+ $value = trim($value, ' \'"');
+ $metaData[$key] = $value;
+ }
+
+ return $metaData;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getAllFlexformFieldsFromFormPlugins(): array
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('tt_content');
+ $queryBuilder->getRestrictions()
+ ->removeAll()
+ ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+
+ $records = $queryBuilder
+ ->select('uid', 'pi_flexform')
+ ->from('tt_content')
+ ->where(
+ $queryBuilder->expr()->eq(
+ 'CType',
+ $queryBuilder->createNamedParameter('form_formframework', \PDO::PARAM_STR)
+ )
+ )
+ ->execute()
+ ->fetchAll();
+
+ return $records;
+ }
+
+ /**
+ * @param int $uid
+ * @param string $flexform
+ * @param bool $updateRefindex
+ */
+ protected function updateContentReference(
+ int $uid,
+ string $flexform,
+ bool $updateRefindex = false
+ ): void {
+ $this->connection->update(
+ 'tt_content',
+ ['pi_flexform' => $flexform],
+ ['uid' => $uid]
+ );
+
+ if (!$updateRefindex) {
+ return;
+ }
+
+ $this->referenceIndex->updateRefIndexTable(
+ 'tt_content',
+ $uid
+ );
+ }
+
+ /**
+ * @return ObjectManager
+ */
+ protected function getObjectManager(): ObjectManager
+ {
+ return GeneralUtility::makeInstance(ObjectManager::class);
+ }
+}
diff --git a/Classes/Updates/v104/MigrateFeloginPlugins.php b/Classes/Updates/v104/MigrateFeloginPlugins.php
new file mode 100644
index 0000000..d833bfd
--- /dev/null
+++ b/Classes/Updates/v104/MigrateFeloginPlugins.php
@@ -0,0 +1,206 @@
+getConnectionForTable('tt_content');
+
+ /** @var QueryBuilder $queryBuilder */
+ $queryBuilder = $connection->createQueryBuilder();
+ $statement = $queryBuilder->select('uid')
+ ->addSelect('pi_flexform')
+ ->from('tt_content')
+ ->where(
+ $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('login'))
+ )
+ ->execute();
+
+ // Update the found record sets
+ while ($record = $statement->fetch()) {
+ $queryBuilder = $connection->createQueryBuilder();
+ $updateResult = $queryBuilder->update('tt_content')
+ ->where(
+ $queryBuilder->expr()->eq(
+ 'uid',
+ $queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
+ )
+ )
+ ->set('pi_flexform', $this->migrateFlexformSettings($record['pi_flexform']))
+ ->execute();
+
+ //exit if at least one update statement is not successful
+ if (!((bool)$updateResult)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Is an update necessary?
+ *
+ * Looks for fe plugins in tt_content table to be migrated
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getConnectionForTable('tt_content')
+ ->createQueryBuilder();
+
+ $queryBuilder->select('pi_flexform')
+ ->from('tt_content')
+ ->where(
+ $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('login')),
+ $this->getFlexformConstraints($queryBuilder)
+ );
+
+ return (bool)$queryBuilder->execute()->fetchColumn();
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ *
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * @param string $oldValue
+ * @return string
+ */
+ protected function migrateFlexformSettings(string $oldValue): string
+ {
+ $fieldNames = implode('|', static::$flexFormFields);
+ $pattern = '//';
+ $replacement = '';
+
+ return preg_replace($pattern, $replacement, $oldValue);
+ }
+
+ /**
+ * Creates a "like" statement for every flexform fields
+ *
+ * @param QueryBuilder $queryBuilder
+ * @return CompositeExpression
+ */
+ protected function getFlexformConstraints(QueryBuilder $queryBuilder): CompositeExpression
+ {
+ $constraints = [];
+
+ foreach (static::$flexFormFields as $flexFormField) {
+ $value = '%%';
+ $constraints[] = $queryBuilder->expr()->like('pi_flexform', $queryBuilder->createNamedParameter($value));
+ }
+
+ return $queryBuilder->expr()->orX(...$constraints);
+ }
+}
diff --git a/Classes/Updates/v104/MigrateFeloginPluginsCtype.php b/Classes/Updates/v104/MigrateFeloginPluginsCtype.php
new file mode 100644
index 0000000..5cf1be4
--- /dev/null
+++ b/Classes/Updates/v104/MigrateFeloginPluginsCtype.php
@@ -0,0 +1,164 @@
+getConnectionForTable('tt_content');
+
+ /** @var QueryBuilder $queryBuilder */
+ $queryBuilder = $connection->createQueryBuilder();
+ $queryBuilder
+ ->update('tt_content')
+ ->set('CType', $this->getNewCType())
+ ->where(
+ $queryBuilder->expr()->eq(
+ 'CType',
+ $queryBuilder->createNamedParameter($this->getOldCType())
+ )
+ )
+ ->execute();
+
+ return true;
+ }
+
+ /**
+ * Is an update necessary?
+ *
+ * If the feature toggle is set: Looks for new fe plugins to be rolled back
+ * Otherwise looks for old record sets to be migrated
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ /** @var QueryBuilder $queryBuilder */
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
+ $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+ $elementCount = $queryBuilder->count('uid')
+ ->from('tt_content')
+ ->where(
+ $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter($this->getOldCType()))
+ )
+ ->execute()->fetchColumn();
+
+ return (bool)$elementCount;
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ *
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ MigrateFeloginPlugins::class
+ ];
+ }
+
+ /**
+ * Checks if feature toggle to use extbase version is enabled
+ *
+ * @return bool
+ */
+ protected function isExtbaseFeatureEnabled(): bool
+ {
+ return GeneralUtility::makeInstance(Features::class)
+ ->isFeatureEnabled('felogin.extbase');
+ }
+
+ /**
+ * Returns the CType that should be replaced by new CType
+ *
+ * @return string
+ */
+ protected function getOldCType(): string
+ {
+ return $this->isExtbaseFeatureEnabled() ? self::CTYPE_PIBASE : self::CTYPE_EXTBASE;
+ }
+
+ /**
+ * Decide which content CType should be used for the current feature toggle state
+ *
+ * @return string
+ */
+ protected function getNewCType(): string
+ {
+ return $this->isExtbaseFeatureEnabled() ? self::CTYPE_EXTBASE : self::CTYPE_PIBASE;
+ }
+}
diff --git a/Classes/Updates/v104/RowUpdater/WorkspaceVersionRecordsMigration.php b/Classes/Updates/v104/RowUpdater/WorkspaceVersionRecordsMigration.php
new file mode 100644
index 0000000..bed1a5a
--- /dev/null
+++ b/Classes/Updates/v104/RowUpdater/WorkspaceVersionRecordsMigration.php
@@ -0,0 +1,97 @@
+ discarded records or archived records. Since we have no connection to the original anymore, we remove them (hard delete)
+ * t3_wsid>0 AND pid=-1 AND t3ver_oid>0 -> find the live version and take the PID from the live version, and replace the PID
+ * Since the move pointer (t3ver_state=3) is not affected, as it contains the future live PID, there is no need to touch these records.
+ *
+ * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
+ */
+class WorkspaceVersionRecordsMigration implements RowUpdaterInterface, LoggerAwareInterface
+{
+ use LoggerAwareTrait;
+
+ public function getTitle(): string
+ {
+ return 'Scan for versioned records and fix their pid, or if no connection to a workspace is given, remove them completely to avoid having them shown on the live website.';
+ }
+
+ /**
+ * @param string $tableName Table name to check
+ * @return bool Return true if a table has workspace enabled
+ */
+ public function hasPotentialUpdateForTable(string $tableName): bool
+ {
+ return BackendUtility::isTableWorkspaceEnabled($tableName);
+ }
+
+ /**
+ * Update "pid" field or delete record completely
+ *
+ * @param string $tableName Table name
+ * @param array $row Given row data
+ * @return array Modified row data
+ */
+ public function updateTableRow(string $tableName, array $row): array
+ {
+ // We only modify records with "pid=-1"
+ if ((int)$row['pid'] !== -1) {
+ return $row;
+ }
+ // pid=-1 and live workspace => this may be very old "previous live" records that should be discarded
+ if ((int)$row['t3ver_wsid'] === 0) {
+ $deleteField = $GLOBALS['TCA'][$tableName]['ctrl']['delete'] ?? 'deleted';
+ $row[$deleteField] = 1;
+ // continue processing versions
+ }
+ // regular versions and placeholders (t3ver_state one of -1, 0, 2, 4 - but not 3) having t3ver_oid set
+ if ((int)$row['t3ver_oid'] > 0 && (int)$row['t3ver_state'] !== VersionState::MOVE_PLACEHOLDER) {
+ // We have a live version, let's connect that one
+ $liveRecord = $this->fetchPageId($tableName, (int)$row['t3ver_oid']);
+ if (is_array($liveRecord)) {
+ $row['pid'] = (int)$liveRecord['pid'];
+ return $row;
+ }
+ }
+ // move placeholder (t3ver_state=3) pointing to live version in t3ver_move_id
+ if ((int)$row['t3ver_move_id'] > 0 && (int)$row['t3ver_state'] === VersionState::MOVE_PLACEHOLDER) {
+ // We have a live version, let's connect that one
+ $liveRecord = $this->fetchPageId($tableName, (int)$row['t3ver_move_id']);
+ if (is_array($liveRecord)) {
+ $row['pid'] = (int)$liveRecord['pid'];
+ return $row;
+ }
+ }
+ // No live version available
+ return $row;
+ }
+
+ protected function fetchPageId(string $tableName, int $id): ?array
+ {
+ return BackendUtility::getRecord($tableName, $id, 'pid');
+ }
+}
diff --git a/Classes/Updates/v104/RsaauthExtractionUpdate.php b/Classes/Updates/v104/RsaauthExtractionUpdate.php
new file mode 100644
index 0000000..b617547
--- /dev/null
+++ b/Classes/Updates/v104/RsaauthExtractionUpdate.php
@@ -0,0 +1,124 @@
+extension = new ExtensionModel(
+ 'rsaauth',
+ 'Deprecated rsaauth extension',
+ '10.0.0',
+ 'friendsoftypo3/rsaauth',
+ 'Contains a service to authenticate TYPO3 BE and FE users using private/public key encryption of passwords.'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'Do not install this extension. Use HTTPS instead. ' . $this->extension->getDescription(),
+ false
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'rsaauthExtension';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install extension "rsaauth" from TER if the site is still not secured using HTTPS';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The extension "rsaauth" adds a public/private key based encryption for Backend and Frontend'
+ . ' login passwords. The approach is limited and has various flaws. The extension is fully'
+ . ' obsolete if the instance uses HTTPS.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !ExtensionManagementUtility::isLoaded('rsaauth');
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+}
diff --git a/Classes/Updates/v104/SysActionExtractionUpdate.php b/Classes/Updates/v104/SysActionExtractionUpdate.php
new file mode 100644
index 0000000..70f99c8
--- /dev/null
+++ b/Classes/Updates/v104/SysActionExtractionUpdate.php
@@ -0,0 +1,123 @@
+extension = new ExtensionModel(
+ 'sys_action',
+ 'Deprecated sys_action extension',
+ '10.0.0',
+ 'friendsoftypo3/sys-action',
+ 'Allows running configured admin tasks in the taskcenter'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'This extension has not been used very often and is only useful if properly configured and in combination with the "taskcenter" extension. ' . $this->extension->getDescription(),
+ false
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'sysActionExtension';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install extension "sys_action" from TER';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The extension "sys_action" adds functionality to make certain Backend admin tasks'
+ . ' available for non-admin users. Extension "taskcenter" must be loaded to use this upgrade wizard.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !ExtensionManagementUtility::isLoaded('sys_action');
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+}
diff --git a/Classes/Updates/v104/TaskcenterExtractionUpdate.php b/Classes/Updates/v104/TaskcenterExtractionUpdate.php
new file mode 100644
index 0000000..da7d098
--- /dev/null
+++ b/Classes/Updates/v104/TaskcenterExtractionUpdate.php
@@ -0,0 +1,123 @@
+extension = new ExtensionModel(
+ 'taskcenter',
+ 'Deprecated taskcenter extension',
+ '10.0.0',
+ 'friendsoftypo3/taskcenter',
+ 'Contains a framework to show and execute registered tasks.'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'This extension has not been used very often and is only useful together with other extensions like sys_action. ' . $this->extension->getDescription(),
+ false
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'taskcenterExtension';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install extension "taskcenter" from TER';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The extension "taskcenter" adds a view for Backend users to run configured tasks.'
+ . ' It is only useful if properly configured.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !ExtensionManagementUtility::isLoaded('taskcenter');
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+}
diff --git a/Classes/Updates/v87/FileReferenceUpdate.php b/Classes/Updates/v87/FileReferenceUpdate.php
index a7b6a99..948c9db 100644
--- a/Classes/Updates/v87/FileReferenceUpdate.php
+++ b/Classes/Updates/v87/FileReferenceUpdate.php
@@ -16,6 +16,7 @@
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Resource\Exception;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceFactory;
@@ -60,13 +61,19 @@ public function getDescription(): string
public function updateNecessary(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
- return (bool)$queryBuilder->count('hash')
+ $queryBuilder->count('hash')
->from('sys_refindex')
->where(
$queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)),
- $queryBuilder->expr()->eq('softref_key', $queryBuilder->createNamedParameter('typolink_tag', \PDO::PARAM_STR)),
- $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
- )
+ $queryBuilder->expr()->eq('softref_key', $queryBuilder->createNamedParameter('typolink_tag', \PDO::PARAM_STR))
+ );
+
+ // Following condition is required for TYPO3 11.0+ Versions, because of BC in TYPO3 v. 11.0, which removes "deleted" column in "sys_refindex" table.
+ // See: https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/11.0/Breaking-93029-DroppedDeletedFieldFromSys_refindex.html
+ if (GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion() < 11) {
+ $queryBuilder->andWhere($queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
+ }
+ return (bool)$queryBuilder
->execute()
->fetchColumn(0);
}
@@ -104,7 +111,7 @@ public function executeUpdate(): bool
$fileReference = $record['ref_string'];
} else {
try {
- $fileObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($record['ref_string']);
+ $fileObject = GeneralUtility::makeInstance(ResourceFactory::class)->retrieveFileOrFolderObject($record['ref_string']);
if ($fileObject instanceof File) {
$fileReference = $fileObject->getUid();
}
diff --git a/Classes/Updates/v87/MigrateFeSessionDataUpdate.php b/Classes/Updates/v87/MigrateFeSessionDataUpdate.php
index 18cc66a..40534c1 100644
--- a/Classes/Updates/v87/MigrateFeSessionDataUpdate.php
+++ b/Classes/Updates/v87/MigrateFeSessionDataUpdate.php
@@ -136,7 +136,6 @@ public function executeUpdate(): bool
// Move records from fe_session_data that are not in fe_sessions
$queryBuilder = $connection->createQueryBuilder();
$selectSQL = $queryBuilder->select('fe_session_data.hash', 'fe_session_data.content', 'fe_session_data.tstamp')
- ->addSelectLiteral('1')
->from('fe_session_data')
->leftJoin(
'fe_session_data',
@@ -151,12 +150,11 @@ public function executeUpdate(): bool
->getSQL();
$insertSQL = sprintf(
- 'INSERT INTO %s(%s, %s, %s, %s) %s',
+ 'INSERT INTO %s(%s, %s, %s) %s',
$connection->quoteIdentifier('fe_sessions'),
$connection->quoteIdentifier('ses_id'),
$connection->quoteIdentifier('ses_data'),
$connection->quoteIdentifier('ses_tstamp'),
- $connection->quoteIdentifier('ses_anonymous'),
$selectSQL
);
diff --git a/Classes/Updates/v87/RowUpdater/ImageCropUpdater.php b/Classes/Updates/v87/RowUpdater/ImageCropUpdater.php
index 0fcc9bc..04f1548 100644
--- a/Classes/Updates/v87/RowUpdater/ImageCropUpdater.php
+++ b/Classes/Updates/v87/RowUpdater/ImageCropUpdater.php
@@ -198,7 +198,7 @@ private function getFile(array $row, $fieldName)
}
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
try {
- $file = ResourceFactory::getInstance()->getFileObject((int)$fileUid);
+ $file = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject((int)$fileUid);
} catch (FileDoesNotExistException $e) {
} catch (\InvalidArgumentException $e) {
}
diff --git a/Classes/Updates/v95/AdminPanelInstall.php b/Classes/Updates/v95/AdminPanelInstall.php
new file mode 100644
index 0000000..70df93b
--- /dev/null
+++ b/Classes/Updates/v95/AdminPanelInstall.php
@@ -0,0 +1,117 @@
+extension = new ExtensionModel(
+ 'adminpanel',
+ 'TYPO3 Admin Panel',
+ '9.2',
+ 'typo3/cms-adminpanel',
+ 'The TYPO3 admin panel provides a panel with additional functionality in the frontend (Debugging, Caching, Preview...)'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'You should install the "adminpanel" only if needed. ' . $this->extension->getDescription(),
+ true
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'adminpanelExtension';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install extension "adminpanel"';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The TYPO3 admin panel was extracted to an own extension. This update installs the extension.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !ExtensionManagementUtility::isLoaded('adminpanel');
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [];
+ }
+}
diff --git a/Classes/Updates/v95/Argon2iPasswordHashes.php b/Classes/Updates/v95/Argon2iPasswordHashes.php
new file mode 100644
index 0000000..e212ccb
--- /dev/null
+++ b/Classes/Updates/v95/Argon2iPasswordHashes.php
@@ -0,0 +1,124 @@
+confirmation = new Confirmation(
+ 'Please make sure to read the following carefully:',
+ $this->getDescription(),
+ false,
+ 'Yes, I understand!',
+ '',
+ true
+ );
+ }
+
+ /**
+ * @return string Unique identifier of this updater
+ */
+ public function getIdentifier(): string
+ {
+ return 'argon2iPasswordHashes';
+ }
+
+ /**
+ * @return string Title of this updater
+ */
+ public function getTitle(): string
+ {
+ return 'Reminder to verify live system supports argon2i';
+ }
+
+ /**
+ * @return string Longer description of this updater
+ */
+ public function getDescription(): string
+ {
+ return 'TYPO3 uses the modern hash mechanism "argon2i" on this system. Existing passwords'
+ . ' will be automatically upgraded to this mechanism upon user login. If this instance'
+ . ' is later deployed to a different system, make sure the system does support argon2i'
+ . ' too, otherwise logins will fail. If that is not possible, select a different hash'
+ . ' algorithm in Setting > Presets > Password hashing settings and make sure no user'
+ . ' has been upgraded yet. This upgrade wizard exists only to inform you, it does not'
+ . ' change the system';
+ }
+
+ /**
+ * Checks whether updates are required.
+ *
+ * @return bool Whether an update is required (TRUE) or not (FALSE)
+ */
+ public function updateNecessary(): bool
+ {
+ $passwordHashFactory = GeneralUtility::makeInstance(PasswordHashFactory::class);
+ $feHash = $passwordHashFactory->getDefaultHashInstance('BE');
+ $beHash = $passwordHashFactory->getDefaultHashInstance('FE');
+ return $feHash instanceof Argon2iPasswordHash || $beHash instanceof Argon2iPasswordHash;
+ }
+
+ /**
+ * @return string[] All new fields and tables must exist
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class,
+ ];
+ }
+
+ /**
+ * This upgrade wizard has informational character only, it does not perform actions.
+ *
+ * @return bool Whether everything went smoothly or not
+ */
+ public function executeUpdate(): bool
+ {
+ return true;
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+}
diff --git a/Classes/Updates/v95/BackendLayoutIconUpdateWizard.php b/Classes/Updates/v95/BackendLayoutIconUpdateWizard.php
new file mode 100644
index 0000000..f2a170c
--- /dev/null
+++ b/Classes/Updates/v95/BackendLayoutIconUpdateWizard.php
@@ -0,0 +1,313 @@
+getRecordsFromTable());
+ }
+
+ /**
+ * @return string[] All new fields and tables must exist
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * @param OutputInterface $output
+ */
+ public function setOutput(OutputInterface $output): void
+ {
+ $this->output = $output;
+ }
+
+ /**
+ * Performs the configuration update.
+ *
+ * @return bool
+ */
+ public function executeUpdate(): bool
+ {
+ $result = true;
+ try {
+ $storages = GeneralUtility::makeInstance(StorageRepository::class)->findAll();
+ $this->storage = $storages[0];
+ $records = $this->getRecordsFromTable();
+ foreach ($records as $record) {
+ $this->migrateField($record);
+ }
+ } catch (\Exception $e) {
+ // If something goes wrong, migrateField() logs an error
+ $result = false;
+ }
+ return $result;
+ }
+
+ /**
+ * Get records from table where the field to migrate is not empty (NOT NULL and != '')
+ * and also not numeric (which means that it is migrated)
+ *
+ * @return array
+ * @throws \RuntimeException
+ */
+ protected function getRecordsFromTable()
+ {
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ $queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
+ $queryBuilder->getRestrictions()->removeAll();
+ try {
+ return $queryBuilder
+ ->select('uid', 'pid', $this->fieldToMigrate)
+ ->from($this->table)
+ ->where(
+ $queryBuilder->expr()->isNotNull($this->fieldToMigrate),
+ $queryBuilder->expr()->neq(
+ $this->fieldToMigrate,
+ $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
+ ),
+ $queryBuilder->expr()->comparison(
+ 'CAST(CAST(' . $queryBuilder->quoteIdentifier($this->fieldToMigrate) . ' AS DECIMAL) AS CHAR)',
+ ExpressionBuilder::NEQ,
+ 'CAST(' . $queryBuilder->quoteIdentifier($this->fieldToMigrate) . ' AS CHAR)'
+ )
+ )
+ ->orderBy('uid')
+ ->execute()
+ ->fetchAll();
+ } catch (DBALException $e) {
+ throw new \RuntimeException(
+ 'Database query failed. Error was: ' . $e->getPrevious()->getMessage(),
+ 1511950673
+ );
+ }
+ }
+
+ /**
+ * Migrates a single field.
+ *
+ * @param array $row
+ * @throws \Exception
+ */
+ protected function migrateField($row)
+ {
+ $fieldItems = GeneralUtility::trimExplode(',', $row[$this->fieldToMigrate], true);
+ if (empty($fieldItems) || is_numeric($row[$this->fieldToMigrate])) {
+ return;
+ }
+ $fileadminDirectory = rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') . '/';
+ $i = 0;
+
+ $storageUid = (int)$this->storage->getUid();
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+
+ foreach ($fieldItems as $item) {
+ $fileUid = null;
+ $sourcePath = Environment::getPublicPath() . '/' . $this->sourcePath . $item;
+ $targetDirectory = Environment::getPublicPath() . '/' . $fileadminDirectory . $this->targetPath;
+ $targetPath = $targetDirectory . PathUtility::basenameDuringBootstrap($item);
+
+ // maybe the file was already moved, so check if the original file still exists
+ if (file_exists($sourcePath)) {
+ if (!is_dir($targetDirectory)) {
+ GeneralUtility::mkdir_deep($targetDirectory);
+ }
+
+ // see if the file already exists in the storage
+ $fileSha1 = sha1_file($sourcePath);
+
+ $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file');
+ $queryBuilder->getRestrictions()->removeAll();
+ $existingFileRecord = $queryBuilder->select('uid')->from('sys_file')->where(
+ $queryBuilder->expr()->eq(
+ 'sha1',
+ $queryBuilder->createNamedParameter($fileSha1, \PDO::PARAM_STR)
+ ),
+ $queryBuilder->expr()->eq(
+ 'storage',
+ $queryBuilder->createNamedParameter($storageUid, \PDO::PARAM_INT)
+ )
+ )->execute()->fetch();
+
+ // the file exists, the file does not have to be moved again
+ if (is_array($existingFileRecord)) {
+ $fileUid = $existingFileRecord['uid'];
+ } else {
+ // just move the file (no duplicate)
+ rename($sourcePath, $targetPath);
+ }
+ }
+
+ if ($fileUid === null) {
+ // get the File object if it hasn't been fetched before
+ try {
+ // if the source file does not exist, we should just continue, but leave a message in the docs;
+ // ideally, the user would be informed after the update as well.
+ /** @var File $file */
+ $file = $this->storage->getFile($this->targetPath . $item);
+ $fileUid = $file->getUid();
+ } catch (\InvalidArgumentException $e) {
+ // no file found, no reference can be set
+ $this->logger->notice(
+ 'File ' . $this->sourcePath . $item . ' does not exist. Reference was not migrated.',
+ [
+ 'table' => $this->table,
+ 'record' => $row,
+ 'field' => $this->fieldToMigrate,
+ ]
+ );
+ $format = 'File \'%s\' does not exist. Referencing field: %s.%d.%s. The reference was not migrated.';
+ $this->output->writeln(sprintf(
+ $format,
+ $this->sourcePath . $item,
+ $this->table,
+ $row['uid'],
+ $this->fieldToMigrate
+ ));
+ continue;
+ }
+ }
+
+ if ($fileUid > 0) {
+ $fields = [
+ 'fieldname' => $this->fieldToMigrate,
+ 'table_local' => 'sys_file',
+ 'pid' => $this->table === 'pages' ? $row['uid'] : $row['pid'],
+ 'uid_foreign' => $row['uid'],
+ 'uid_local' => $fileUid,
+ 'tablenames' => $this->table,
+ 'crdate' => time(),
+ 'tstamp' => time(),
+ 'sorting_foreign' => $i,
+ ];
+
+ $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_reference');
+ $queryBuilder->insert('sys_file_reference')->values($fields)->execute();
+ ++$i;
+ }
+ }
+
+ // Update referencing table's original field to now contain the count of references,
+ // but only if all new references could be set
+ if ($i === count($fieldItems)) {
+ $queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
+ $queryBuilder->update($this->table)->where(
+ $queryBuilder->expr()->eq(
+ 'uid',
+ $queryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT)
+ )
+ )->set($this->fieldToMigrate, $i)->execute();
+ }
+ }
+}
diff --git a/Classes/Updates/v95/BackendUserConfigurationUpdate.php b/Classes/Updates/v95/BackendUserConfigurationUpdate.php
new file mode 100644
index 0000000..d4255a5
--- /dev/null
+++ b/Classes/Updates/v95/BackendUserConfigurationUpdate.php
@@ -0,0 +1,153 @@
+getAffectedBackendUsers() as $backendUser) {
+ $userConfig = $this->unserializeUserConfig($backendUser['uc']);
+
+ if (!is_array($userConfig)) {
+ continue;
+ }
+
+ array_walk_recursive($userConfig, function (&$item) use (&$needsExecution) {
+ if ($item instanceof \stdClass) {
+ $needsExecution = true;
+ }
+ });
+
+ if ($needsExecution) {
+ break;
+ }
+ }
+
+ return $needsExecution;
+ }
+
+ /**
+ * @return string[] All new fields and tables must exist
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * Performs the database update for be_users
+ *
+ * @return bool
+ */
+ public function executeUpdate(): bool
+ {
+ foreach ($this->getAffectedBackendUsers() as $backendUser) {
+ $userConfig = $this->unserializeUserConfig($backendUser['uc']);
+
+ if (!is_array($userConfig)) {
+ continue;
+ }
+
+ array_walk_recursive($userConfig, function (&$item) {
+ if ($item instanceof \stdClass) {
+ $item = json_decode(json_encode($item), true);
+ }
+ });
+
+ $this->updateBackendUser((int)$backendUser['uid'], $userConfig);
+ }
+
+ return true;
+ }
+
+ private function unserializeUserConfig(string $userConfig)
+ {
+ return unserialize($userConfig, ['allowed_classes' => [\stdClass::class]]);
+ }
+
+ private function getAffectedBackendUsers(): iterable
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('be_users');
+ $queryBuilder->getRestrictions()->removeAll();
+ $statement = $queryBuilder
+ ->select('uid', 'uc')
+ ->from('be_users')
+ ->where(
+ $queryBuilder->expr()->like(
+ 'uc',
+ $queryBuilder->createNamedParameter(
+ '%"stdClass"%'
+ )
+ )
+ );
+
+ return $statement->execute();
+ }
+
+ private function updateBackendUser(int $userId, array $userConfig): void
+ {
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users');
+ $connection->update('be_users', ['uc' => serialize($userConfig)], ['uid' => $userId], [\PDO::PARAM_LOB, \PDO::PARAM_INT]);
+ }
+}
diff --git a/Classes/Updates/v95/FuncExtractionUpdate.php b/Classes/Updates/v95/FuncExtractionUpdate.php
new file mode 100644
index 0000000..0395137
--- /dev/null
+++ b/Classes/Updates/v95/FuncExtractionUpdate.php
@@ -0,0 +1,118 @@
+extension = new ExtensionModel(
+ 'func',
+ 'Web->Functions module',
+ '9.0.1',
+ 'friendsoftypo3/cms-func',
+ 'Provides Web->Functions BE module used in previous TYPO3 versions for extensions that still rely on it.'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'You should install EXT:func only if you really need it. ' . $this->extension->getDescription(),
+ false
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'funcExtension';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install extension "func" from TER';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The extension "func" that brings the "Web->Functions" backend module has been extracted to'
+ . ' the TYPO3 Extension Repository. This update downloads the TYPO3 extension func from the TER.'
+ . ' Use this if you\'re dealing with extensions in the instance that rely on "Web->Functions" and bring own'
+ . ' modules.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !ExtensionManagementUtility::isLoaded($this->extension->getKey());
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [];
+ }
+}
diff --git a/Classes/Updates/v95/MigratePagesLanguageOverlayBeGroupsAccessRights.php b/Classes/Updates/v95/MigratePagesLanguageOverlayBeGroupsAccessRights.php
new file mode 100644
index 0000000..8268d5d
--- /dev/null
+++ b/Classes/Updates/v95/MigratePagesLanguageOverlayBeGroupsAccessRights.php
@@ -0,0 +1,146 @@
+getQueryBuilderForTable(
+ 'be_groups'
+ );
+ $beGroupsQueryBuilder->getRestrictions()->removeAll();
+ $beGroupsRows = $beGroupsQueryBuilder
+ ->select('uid', 'non_exclude_fields', 'tables_modify')
+ ->from('be_groups')
+ ->execute();
+ while ($beGroupsRow = $beGroupsRows->fetch()) {
+ $updateNeeded = false;
+ if (!empty($beGroupsRow['tables_modify'])) {
+ // If 'pages_language_overlay' is allowed as table-modify, remove it and add
+ // 'pages' if it is not in there, yet.
+ $tablesArray = GeneralUtility::trimExplode(',', $beGroupsRow['tables_modify'], true);
+ $newTablesArray = $tablesArray;
+ if (in_array('pages_language_overlay', $tablesArray, true)) {
+ $updateNeeded = true;
+ $newTablesArray = array_diff($tablesArray, ['pages_language_overlay']);
+ if (!in_array('pages', $newTablesArray, true)) {
+ $newTablesArray[] = 'pages';
+ }
+ }
+ } else {
+ $newTablesArray = [];
+ }
+ if (!empty($beGroupsRow['non_exclude_fields'])) {
+ // Exclude fields on 'pages_language_overlay' are removed and added as
+ // exclude fields on 'pages'
+ $excludeFields = GeneralUtility::trimExplode(',', $beGroupsRow['non_exclude_fields'], true);
+ $newExcludeFields = [];
+ foreach ($excludeFields as $tableFieldCombo) {
+ if (strpos($tableFieldCombo, 'pages_language_overlay:') === 0) {
+ $updateNeeded = true;
+ $field = substr($tableFieldCombo, strlen('pages_language_overlay:'));
+ $newExcludeFields[] = 'pages:' . $field;
+ } else {
+ $newExcludeFields[] = $tableFieldCombo;
+ }
+ }
+ array_unique($newExcludeFields);
+ } else {
+ $newExcludeFields = [];
+ }
+ if ($updateNeeded) {
+ $updateBeGroupsQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('be_groups');
+ $updateBeGroupsQueryBuilder
+ ->update('be_groups')
+ ->set('tables_modify', implode(',', $newTablesArray))
+ ->set('non_exclude_fields', implode(',', $newExcludeFields))
+ ->where(
+ $updateBeGroupsQueryBuilder->expr()->eq(
+ 'uid',
+ $updateBeGroupsQueryBuilder->createNamedParameter($beGroupsRow['uid'], \PDO::PARAM_INT)
+ )
+ )
+ ->execute();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !(new UpgradeWizardsService())->isWizardDone($this->getIdentifier());
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ public function getDescription(): string
+ {
+ return 'The table pages_language_overlay will be removed to align the translation ' .
+ 'handling for pages with the rest of the core. This wizard transfers all be_groups with ' .
+ 'access restrictions to pages_language_overlay into pages.';
+ }
+
+ /**
+ * @return Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return GeneralUtility::makeInstance(
+ Confirmation::class,
+ 'Are you sure?',
+ 'Do you want to continue?',
+ false
+ );
+ }
+}
diff --git a/Classes/Updates/v95/MigratePagesLanguageOverlayUpdate.php b/Classes/Updates/v95/MigratePagesLanguageOverlayUpdate.php
new file mode 100644
index 0000000..e4daa85
--- /dev/null
+++ b/Classes/Updates/v95/MigratePagesLanguageOverlayUpdate.php
@@ -0,0 +1,351 @@
+checkIfWizardIsRequired()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return string[] All new fields and tables must exist
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * Additional output if there are columns with mm config
+ *
+ * @param OutputInterface $output
+ */
+ public function setOutput(OutputInterface $output): void
+ {
+ $this->output = $output;
+ }
+
+ /**
+ * Performs the update.
+ *
+ * @return bool Whether everything went smoothly or not
+ */
+ public function executeUpdate(): bool
+ {
+ // Warn for TCA relation configurations which are not migrated.
+ if (isset($GLOBALS['TCA']['pages_language_overlay']['columns'])
+ && is_array($GLOBALS['TCA']['pages_language_overlay']['columns'])
+ ) {
+ foreach ($GLOBALS['TCA']['pages_language_overlay']['columns'] as $fieldName => $fieldConfiguration) {
+ if (isset($fieldConfiguration['config']['MM'])) {
+ $this->output->writeln('The pages_language_overlay field ' . $fieldName
+ . ' with its MM relation configuration can not be migrated'
+ . ' automatically. Existing data relations to this field have'
+ . ' to be migrated manually.');
+ }
+ }
+ }
+
+ // Ensure pages_language_overlay is still available in TCA
+ GeneralUtility::makeInstance(LoadTcaService::class)->loadExtensionTablesWithoutMigration();
+ $this->mergePagesLanguageOverlayIntoPages();
+ $this->updateInlineRelations();
+ $this->updateSysHistoryRelations();
+ return true;
+ }
+
+ /**
+ * 1. Fetches ALL pages_language_overlay (= translations) records
+ * 2. Fetches the given page record (= original language) for each translation
+ * 3. Populates the values from the original language IF the field in the translation record is NOT SET (empty is fine)
+ * 4. Adds proper fields for the translations which is
+ * - l10n_parent = UID of the original-language-record
+ * - pid = PID of the original-language-record (please note: THIS IS DIFFERENT THAN IN pages_language_overlay)
+ * - l10n_source = UID of the original-language-record (only this is supported currently)
+ */
+ protected function mergePagesLanguageOverlayIntoPages()
+ {
+ $overlayQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages_language_overlay');
+ $overlayQueryBuilder->getRestrictions()->removeAll();
+ $overlayRecords = $overlayQueryBuilder
+ ->select('*')
+ ->from('pages_language_overlay')
+ ->execute();
+ $pagesConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('pages');
+ $pagesColumns = $pagesConnection->getSchemaManager()->listTableDetails('pages')->getColumns();
+ $pagesColumnTypes = [];
+ foreach ($pagesColumns as $pageColumn) {
+ $pagesColumnTypes[$pageColumn->getName()] = $pageColumn->getType()->getBindingType();
+ }
+ while ($overlayRecord = $overlayRecords->fetch()) {
+ // Early continue if record has been migrated before
+ if ($this->isOverlayRecordMigratedAlready((int)$overlayRecord['uid'])) {
+ continue;
+ }
+
+ $values = [];
+ $originalPageId = (int)$overlayRecord['pid'];
+ $page = $this->fetchDefaultLanguagePageRecord($originalPageId);
+ if (!empty($page)) {
+ foreach ($pagesColumns as $pageColumn) {
+ $name = $pageColumn->getName();
+ if (isset($overlayRecord[$name])) {
+ $values[$name] = $overlayRecord[$name];
+ } elseif (isset($page[$name])) {
+ $values[$name] = $page[$name];
+ }
+ }
+
+ $values['pid'] = $page['pid'];
+ $values['l10n_parent'] = $originalPageId;
+ $values['l10n_source'] = $originalPageId;
+ $values['legacy_overlay_uid'] = $overlayRecord['uid'];
+ unset($values['uid']);
+ $pagesConnection->insert(
+ 'pages',
+ $values,
+ $pagesColumnTypes
+ );
+ }
+ }
+ }
+
+ /**
+ * Inline relations with foreign_field, foreign_table, foreign_table_field on
+ * pages_language_overlay TCA get their existing relations updated to new
+ * uid and pages table.
+ */
+ protected function updateInlineRelations()
+ {
+ if (isset($GLOBALS['TCA']['pages_language_overlay']['columns']) && is_array($GLOBALS['TCA']['pages_language_overlay']['columns'])) {
+ foreach ($GLOBALS['TCA']['pages_language_overlay']['columns'] as $fieldName => $fieldConfiguration) {
+ // Migrate any 1:n relations
+ if ($fieldConfiguration['config']['type'] === 'inline'
+ && !empty($fieldConfiguration['config']['foreign_field'])
+ && !empty($fieldConfiguration['config']['foreign_table'])
+ && !empty($fieldConfiguration['config']['foreign_table_field'])
+ ) {
+ $foreignTable = trim($fieldConfiguration['config']['foreign_table']);
+ $foreignField = trim($fieldConfiguration['config']['foreign_field']);
+ $foreignTableField = trim($fieldConfiguration['config']['foreign_table_field']);
+ $translatedPagesQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
+ $translatedPagesQueryBuilder->getRestrictions()->removeAll();
+ $translatedPagesRows = $translatedPagesQueryBuilder
+ ->select('uid', 'legacy_overlay_uid')
+ ->from('pages')
+ ->where(
+ $translatedPagesQueryBuilder->expr()->gt(
+ 'l10n_parent',
+ $translatedPagesQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+ )
+ )
+ ->execute();
+ while ($translatedPageRow = $translatedPagesRows->fetch()) {
+ $foreignTableQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($foreignTable);
+ $foreignTableQueryBuilder->getRestrictions()->removeAll();
+ $foreignTableQueryBuilder
+ ->update($foreignTable)
+ ->set($foreignField, $translatedPageRow['uid'])
+ ->set($foreignTableField, 'pages')
+ ->where(
+ $foreignTableQueryBuilder->expr()->eq(
+ $foreignField,
+ $foreignTableQueryBuilder->createNamedParameter($translatedPageRow['legacy_overlay_uid'], \PDO::PARAM_INT)
+ ),
+ $foreignTableQueryBuilder->expr()->eq(
+ $foreignTableField,
+ $foreignTableQueryBuilder->createNamedParameter('pages_language_overlay', \PDO::PARAM_STR)
+ )
+ )
+ ->execute();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Update recuid and tablename of sys_history table to pages and new uid
+ * for all pages_language_overlay rows
+ */
+ protected function updateSysHistoryRelations()
+ {
+ $translatedPagesQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
+ $translatedPagesQueryBuilder->getRestrictions()->removeAll();
+ $translatedPagesRows = $translatedPagesQueryBuilder
+ ->select('uid', 'legacy_overlay_uid')
+ ->from('pages')
+ ->where(
+ $translatedPagesQueryBuilder->expr()->gt(
+ 'l10n_parent',
+ $translatedPagesQueryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+ )
+ )
+ ->execute();
+ while ($translatedPageRow = $translatedPagesRows->fetch()) {
+ $historyTableQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
+ $historyTableQueryBuilder->getRestrictions()->removeAll();
+ $historyTableQueryBuilder
+ ->update('sys_history')
+ ->set('tablename', 'pages')
+ ->set('recuid', $translatedPageRow['uid'])
+ ->where(
+ $historyTableQueryBuilder->expr()->eq(
+ 'recuid',
+ $historyTableQueryBuilder->createNamedParameter($translatedPageRow['legacy_overlay_uid'], \PDO::PARAM_INT)
+ ),
+ $historyTableQueryBuilder->expr()->eq(
+ 'tablename',
+ $historyTableQueryBuilder->createNamedParameter('pages_language_overlay', \PDO::PARAM_STR)
+ )
+ )
+ ->execute();
+ }
+ }
+
+ /**
+ * Fetches a certain page
+ *
+ * @param int $pageId
+ * @return array
+ */
+ protected function fetchDefaultLanguagePageRecord(int $pageId): array
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
+ $queryBuilder->getRestrictions()->removeAll();
+ $page = $queryBuilder
+ ->select('*')
+ ->from('pages')
+ ->where(
+ $queryBuilder->expr()->eq(
+ 'uid',
+ $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
+ )
+ )
+ ->execute()
+ ->fetch();
+ return $page ?: [];
+ }
+
+ /**
+ * Verify if a single overlay record has been migrated to pages already
+ * by checking the db field legacy_overlay_uid for the orig uid
+ *
+ * @param int $overlayUid
+ * @return bool
+ */
+ protected function isOverlayRecordMigratedAlready(int $overlayUid): bool
+ {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
+ $queryBuilder->getRestrictions()->removeAll();
+ $migratedRecord = $queryBuilder
+ ->select('uid')
+ ->from('pages')
+ ->where(
+ $queryBuilder->expr()->eq(
+ 'legacy_overlay_uid',
+ $queryBuilder->createNamedParameter($overlayUid, \PDO::PARAM_INT)
+ )
+ )
+ ->execute()
+ ->fetch();
+ return !empty($migratedRecord);
+ }
+
+ /**
+ * Check if the database table "pages_language_overlay" exists and if so, if there are entries in the DB table.
+ *
+ * @return bool
+ * @throws \InvalidArgumentException
+ */
+ protected function checkIfWizardIsRequired(): bool
+ {
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ $connection = $connectionPool->getConnectionByName('Default');
+ $tableNames = $connection->getSchemaManager()->listTableNames();
+ if (in_array('pages_language_overlay', $tableNames, true)) {
+ // table is available, now check if there are entries in it
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('pages_language_overlay');
+ $numberOfEntries = $queryBuilder->count('*')
+ ->from('pages_language_overlay')
+ ->execute()
+ ->fetchColumn();
+ return (bool)$numberOfEntries;
+ }
+
+ return false;
+ }
+}
diff --git a/Classes/Updates/v95/MigrateUrlTypesInPagesUpdate.php b/Classes/Updates/v95/MigrateUrlTypesInPagesUpdate.php
new file mode 100644
index 0000000..5e38882
--- /dev/null
+++ b/Classes/Updates/v95/MigrateUrlTypesInPagesUpdate.php
@@ -0,0 +1,167 @@
+checkIfWizardIsRequired()) {
+ return false;
+ }
+ $recordsToMigrate = 0;
+ // Check if there is data to migrate
+ foreach ($this->databaseTables as $databaseTable) {
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable($databaseTable);
+ $queryBuilder->getRestrictions()->removeAll();
+ $recordsToMigrate = $queryBuilder->count('*')
+ ->from($databaseTable)
+ ->where(
+ $queryBuilder->expr()->neq('urltype', 0),
+ $queryBuilder->expr()->neq('url', $queryBuilder->createPositionalParameter(''))
+ )
+ ->execute()
+ ->fetchColumn();
+
+ if ($recordsToMigrate > 0) {
+ break;
+ }
+ }
+ return $recordsToMigrate > 0;
+ }
+
+ /**
+ * @return string[] All new fields and tables must exist
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * Moves data from pages.urltype to pages.url
+ *
+ * @return bool
+ */
+ public function executeUpdate(): bool
+ {
+ foreach ($this->databaseTables as $databaseTable) {
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getConnectionForTable($databaseTable);
+
+ // Process records that have entries in pages.urltype
+ $queryBuilder = $connection->createQueryBuilder();
+ $queryBuilder->getRestrictions()->removeAll();
+ $statement = $queryBuilder->select('uid', 'urltype', 'url')
+ ->from($databaseTable)
+ ->where(
+ $queryBuilder->expr()->neq('urltype', 0),
+ $queryBuilder->expr()->neq('url', $queryBuilder->createPositionalParameter(''))
+ )
+ ->execute();
+
+ while ($row = $statement->fetch()) {
+ $url = $this->urltypes[(int)$row['urltype']] . $row['url'];
+ $updateQueryBuilder = $connection->createQueryBuilder();
+ $updateQueryBuilder
+ ->update($databaseTable)
+ ->where(
+ $updateQueryBuilder->expr()->eq(
+ 'uid',
+ $updateQueryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT)
+ )
+ )
+ ->set('url', $updateQueryBuilder->createNamedParameter($url), false)
+ ->set('urltype', 0);
+ $updateQueryBuilder->execute();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check each table if the column exists
+ *
+ * @return bool
+ */
+ protected function checkIfWizardIsRequired(): bool
+ {
+ foreach ($this->databaseTables as $key => $databaseTable) {
+ $columns = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getConnectionForTable($databaseTable)
+ ->getSchemaManager()
+ ->listTableColumns($databaseTable);
+ if (!isset($columns['urltype'])) {
+ unset($this->databaseTables[$key]);
+ }
+ }
+ return count($this->databaseTables) > 0;
+ }
+}
diff --git a/Classes/Updates/v95/PopulatePageSlugs.php b/Classes/Updates/v95/PopulatePageSlugs.php
new file mode 100644
index 0000000..dbada05
--- /dev/null
+++ b/Classes/Updates/v95/PopulatePageSlugs.php
@@ -0,0 +1,290 @@
+ generate the slug.
+ *
+ * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
+ */
+class PopulatePageSlugs implements UpgradeWizardInterface
+{
+ /**
+ * @var string
+ */
+ protected $table = 'pages';
+
+ /**
+ * @var string
+ */
+ protected $fieldName = 'slug';
+
+ /**
+ * @return string Unique identifier of this updater
+ */
+ public function getIdentifier(): string
+ {
+ return 'pagesSlugs';
+ }
+
+ /**
+ * @return string Title of this updater
+ */
+ public function getTitle(): string
+ {
+ return 'Introduce URL parts ("slugs") to all existing pages';
+ }
+
+ /**
+ * @return string Longer description of this updater
+ */
+ public function getDescription(): string
+ {
+ return 'TYPO3 includes native URL handling. Every page record has its own speaking URL path'
+ . ' called "slug" which can be edited in TYPO3 Backend. However, it is necessary that all pages have'
+ . ' a URL pre-filled. This is done by evaluating the page title / navigation title and all of its rootline.';
+ }
+
+ /**
+ * Checks whether updates are required.
+ *
+ * @return bool Whether an update is required (TRUE) or not (FALSE)
+ */
+ public function updateNecessary(): bool
+ {
+ $updateNeeded = false;
+ // Check if the database table even exists
+ if ($this->checkIfWizardIsRequired()) {
+ $updateNeeded = true;
+ }
+ return $updateNeeded;
+ }
+
+ /**
+ * @return string[] All new fields and tables must exist
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * Performs the accordant updates.
+ *
+ * @return bool Whether everything went smoothly or not
+ */
+ public function executeUpdate(): bool
+ {
+ $this->populateSlugs();
+ return true;
+ }
+
+ /**
+ * Fills the database table "pages" with slugs based on the page title and its configuration.
+ * But also checks "legacy" functionality.
+ */
+ protected function populateSlugs()
+ {
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
+ $queryBuilder = $connection->createQueryBuilder();
+ $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+ $statement = $queryBuilder
+ ->select('*')
+ ->from($this->table)
+ ->where(
+ $queryBuilder->expr()->orX(
+ $queryBuilder->expr()->eq($this->fieldName, $queryBuilder->createNamedParameter('')),
+ $queryBuilder->expr()->isNull($this->fieldName)
+ )
+ )
+ // Ensure that live workspace records are handled first
+ ->addOrderBy('t3ver_wsid', 'asc')
+ // Ensure that all pages are run through "per parent page" field, and in the correct sorting values
+ ->addOrderBy('pid', 'asc')
+ ->addOrderBy('sorting', 'asc')
+ ->execute();
+
+ // Check for existing slugs from realurl
+ $suggestedSlugs = [];
+ if ($this->checkIfTableExists('tx_realurl_pathdata')) {
+ $suggestedSlugs = $this->getSuggestedSlugs('tx_realurl_pathdata');
+ } elseif ($this->checkIfTableExists('tx_realurl_pathcache')) {
+ $suggestedSlugs = $this->getSuggestedSlugs('tx_realurl_pathcache', 'cache_id');
+ }
+
+ $fieldConfig = $this->getSlugFieldConfig();
+ $evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : [];
+ $hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true);
+ $hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true);
+ $slugHelper = GeneralUtility::makeInstance(SlugHelper::class, $this->table, $this->fieldName, $fieldConfig);
+ while ($record = $statement->fetch()) {
+ $recordId = (int)$record['uid'];
+ $pid = (int)$record['pid'];
+ $languageId = (int)$record['sys_language_uid'];
+ $pageIdInDefaultLanguage = $languageId > 0 ? (int)$record['l10n_parent'] : $recordId;
+ $slug = $suggestedSlugs[$pageIdInDefaultLanguage][$languageId] ?? '';
+
+ // see if an alias field was used, then let's build a slug out of that. This field does not exist in v10
+ // anymore, so this will only be necessary in edge-cases when upgrading from earlier versions with aliases
+ if (!empty($record['alias'])) {
+ $slug = $slugHelper->sanitize('/' . $record['alias']);
+ }
+
+ if (empty($slug)) {
+ // Resolve the live "pid"
+ if ($record['t3ver_oid'] > 0) {
+ $queryBuilder = $connection->createQueryBuilder();
+ $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+ $liveVersion = $queryBuilder
+ ->select('pid')
+ ->from('pages')
+ ->where(
+ $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($record['t3ver_oid'], \PDO::PARAM_INT))
+ )->execute()->fetch();
+ $pid = (int)$liveVersion['pid'];
+ }
+ $slug = $slugHelper->generate($record, $pid);
+ }
+
+ $state = RecordStateFactory::forName($this->table)
+ ->fromArray($record, $pid, $recordId);
+ if ($hasToBeUniqueInSite && !$slugHelper->isUniqueInSite($slug, $state)) {
+ $slug = $slugHelper->buildSlugForUniqueInSite($slug, $state);
+ }
+ if ($hasToBeUniqueInPid && !$slugHelper->isUniqueInPid($slug, $state)) {
+ $slug = $slugHelper->buildSlugForUniqueInPid($slug, $state);
+ }
+
+ $connection->update(
+ $this->table,
+ [$this->fieldName => $slug],
+ ['uid' => $recordId]
+ );
+ }
+ }
+
+ /**
+ * Check if there are record within "pages" database table with an empty "slug" field.
+ *
+ * @return bool
+ * @throws \InvalidArgumentException
+ */
+ protected function checkIfWizardIsRequired(): bool
+ {
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ $queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
+ $queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
+
+ $numberOfEntries = $queryBuilder
+ ->count('uid')
+ ->from($this->table)
+ ->where(
+ $queryBuilder->expr()->orX(
+ $queryBuilder->expr()->eq($this->fieldName, $queryBuilder->createNamedParameter('')),
+ $queryBuilder->expr()->isNull($this->fieldName)
+ )
+ )
+ ->execute()
+ ->fetchColumn();
+ return $numberOfEntries > 0;
+ }
+
+ /**
+ * Resolve prepared realurl "pagepath" for pages
+ *
+ * @param string $tableName
+ * @param string $identityField
+ * @return array with pageID (default language) and language ID as two-dimensional array containing the page path
+ */
+ protected function getSuggestedSlugs(string $tableName, string $identityField = 'uid'): array
+ {
+ $context = GeneralUtility::makeInstance(Context::class);
+ $currentTimestamp = $context->getPropertyFromAspect('date', 'timestamp');
+
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
+ $statement = $queryBuilder
+ ->select('*')
+ ->from($tableName)
+ ->where(
+ $queryBuilder->expr()->eq('mpvar', $queryBuilder->createNamedParameter('')),
+ $queryBuilder->expr()->orX(
+ $queryBuilder->expr()->eq('expire', $queryBuilder->createNamedParameter(0)),
+ $queryBuilder->expr()->gt('expire', $queryBuilder->createNamedParameter($currentTimestamp))
+ )
+ )
+ ->orderBy('expire', 'ASC')
+ ->execute();
+ $suggestedSlugs = [];
+ while ($row = $statement->fetch()) {
+ // rawurldecode ensures that non-ASCII arguments are also migrated
+ $pagePath = rawurldecode($row['pagepath']);
+ if (!isset($suggestedSlugs[(int)$row['page_id']][(int)$row['language_id']])) { // keep only first result
+ $suggestedSlugs[(int)$row['page_id']][(int)$row['language_id']] = '/' . trim($pagePath, '/');
+ }
+ }
+ return $suggestedSlugs;
+ }
+
+ /**
+ * Check if given table exists
+ *
+ * @param string $table
+ * @return bool
+ */
+ protected function checkIfTableExists($table)
+ {
+ $tableExists = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getConnectionForTable($table)
+ ->getSchemaManager()
+ ->tablesExist([$table]);
+
+ return $tableExists;
+ }
+
+ /**
+ * Load the TCA field configuration for the slug field
+ * and add further static fields to generatorOptions.
+ *
+ * @return array
+ */
+ protected function getSlugFieldConfig(): array
+ {
+ $fieldConfig = $GLOBALS['TCA'][$this->table]['columns'][$this->fieldName]['config'];
+
+ // Add the EXT:realurl specific field to generatorOptions
+ $fieldConfig['generatorOptions']['fields'] = ['tx_realurl_pathsegment,title'];
+
+ return $fieldConfig;
+ }
+}
diff --git a/Classes/Updates/v95/RedirectExtractionUpdate.php b/Classes/Updates/v95/RedirectExtractionUpdate.php
new file mode 100644
index 0000000..4fdad00
--- /dev/null
+++ b/Classes/Updates/v95/RedirectExtractionUpdate.php
@@ -0,0 +1,151 @@
+extension = new ExtensionModel(
+ 'rdct',
+ 'Redirects based on &RDCT parameter',
+ '1.0.0',
+ 'friendsoftypo3/rdct',
+ 'The extension provides redirects based on "cache_md5params" and the GET parameter &RDCT for extensions that still rely on it.'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'You should install the Redirects extension only if needed. ' . $this->extension->getDescription(),
+ false
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'rdctExtension';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install extension "rdct" from TER if DB table cache_md5params is filled';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The extension "rdct" includes redirects based on the GET parameter &RDCT. The functionality has been extracted to'
+ . ' the TYPO3 Extension Repository. This update downloads the TYPO3 extension from the TER.'
+ . ' Use this if you are dealing with extensions in the instance that rely on this kind of redirects.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !ExtensionManagementUtility::isLoaded('rdct') && $this->checkIfWizardIsRequired();
+ }
+
+ /**
+ * Check if the database table "cache_md5params" exists and if so, if there are entries in the DB table.
+ *
+ * @return bool
+ * @throws \InvalidArgumentException
+ */
+ protected function checkIfWizardIsRequired(): bool
+ {
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ $connection = $connectionPool->getConnectionByName('Default');
+ $tableNames = $connection->getSchemaManager()->listTableNames();
+ if (in_array('cache_md5params', $tableNames, true)) {
+ // table is available, now check if there are entries in it
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('cache_md5params');
+ $numberOfEntries = $queryBuilder->count('*')
+ ->from('cache_md5params')
+ ->execute()
+ ->fetchColumn();
+ return (bool)$numberOfEntries;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+}
diff --git a/Classes/Updates/v95/RedirectsExtensionUpdate.php b/Classes/Updates/v95/RedirectsExtensionUpdate.php
new file mode 100644
index 0000000..31dc0e1
--- /dev/null
+++ b/Classes/Updates/v95/RedirectsExtensionUpdate.php
@@ -0,0 +1,252 @@
+extension = new ExtensionModel(
+ 'redirects',
+ 'Redirects',
+ '9.2',
+ 'typo3/cms-redirects',
+ 'Manage redirects for your TYPO3-based website'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'You should install the "redirects" extension only if needed. ' . $this->extension->getDescription(),
+ true
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'redirects';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install system extension "redirects" if a sys_domain entry with redirectTo is necessary';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The extension "redirects" includes functionality to handle any kind of redirects. '
+ . 'The functionality supersedes sys_domain entries with the only purpose of redirecting to a different domain or entry. '
+ . 'This upgrade wizard installs the redirect extension if necessary and migrates the sys_domain entries to standard redirects.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return $this->checkIfWizardIsRequired();
+ }
+
+ /**
+ * Performs the update:
+ * - Install EXT:redirect
+ * - Migrate DB records
+ *
+ * @return bool
+ */
+ public function executeUpdate(): bool
+ {
+ // Install the EXT:redirects extension if not happened yet
+ $installationSuccessful = $this->installExtension($this->extension);
+ if ($installationSuccessful) {
+ // Migrate the database entries
+ $this->migrateRedirectDomainsToSysRedirect();
+ }
+ return $installationSuccessful;
+ }
+
+ /**
+ * Check if the database field "sys_domain.redirectTo" exists and if so, if there are entries in the DB table with the field filled.
+ *
+ * @return bool
+ * @throws \InvalidArgumentException
+ */
+ protected function checkIfWizardIsRequired(): bool
+ {
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ $connection = $connectionPool->getConnectionByName('Default');
+ $tables = $connection->getSchemaManager()->listTables();
+ $tableExists = false;
+ foreach ($tables as $table) {
+ if (strtolower($table->getName()) === 'sys_domain') {
+ $tableExists = true;
+ }
+ }
+ if (!$tableExists) {
+ return false;
+ }
+ $columns = $connection->getSchemaManager()->listTableColumns('sys_domain');
+ if (isset($columns['redirectto'])) {
+ // table is available, now check if there are entries in it
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('sys_domain');
+ $queryBuilder->getRestrictions()->removeAll();
+ $numberOfEntries = $queryBuilder->count('*')
+ ->from('sys_domain')
+ ->where(
+ $queryBuilder->expr()->neq('redirectTo', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR))
+ )
+ ->execute()
+ ->fetchColumn();
+ return (bool)$numberOfEntries;
+ }
+
+ return false;
+ }
+
+ /**
+ * Move all sys_domain records with a "redirectTo" value filled (also deleted) to "sys_redirect" record
+ */
+ protected function migrateRedirectDomainsToSysRedirect()
+ {
+ $connDomains = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_domain');
+ $connRedirects = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_redirect');
+
+ $queryBuilder = $connDomains->createQueryBuilder();
+ $queryBuilder->getRestrictions()->removeAll();
+ $domainEntries = $queryBuilder->select('*')
+ ->from('sys_domain')
+ ->where(
+ $queryBuilder->expr()->neq('redirectTo', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR))
+ )
+ ->execute()
+ ->fetchAll();
+
+ foreach ($domainEntries as $domainEntry) {
+ $domainName = $domainEntry['domainName'];
+ $target = $domainEntry['redirectTo'];
+ $sourceDetails = $this->getDomainDetails($domainName);
+ $targetDetails = $this->getDomainDetails($target);
+ $redirectRecord = [
+ 'deleted' => (int)$domainEntry['deleted'],
+ 'disabled' => (int)$domainEntry['hidden'],
+ 'createdon' => (int)$domainEntry['crdate'],
+ 'createdby' => (int)$domainEntry['cruser_id'],
+ 'updatedon' => (int)$domainEntry['tstamp'],
+ 'source_host' => $sourceDetails['host'] . ($sourceDetails['port'] ? ':' . $sourceDetails['port'] : ''),
+ 'keep_query_parameters' => (int)$domainEntry['prepend_params'],
+ 'target_statuscode' => (int)$domainEntry['redirectHttpStatusCode'],
+ 'target' => $target
+ ];
+
+ if (isset($targetDetails['scheme']) && $targetDetails['scheme'] === 'https') {
+ $redirectRecord['force_https'] = 1;
+ }
+
+ if (empty($sourceDetails['path']) || $sourceDetails['path'] === '/') {
+ $redirectRecord['source_path'] = '#.*#';
+ $redirectRecord['is_regexp'] = 1;
+ } else {
+ // Remove the / and add a "/" always before, and at the very end, if path is not empty
+ $sourceDetails['path'] = trim($sourceDetails['path'], '/');
+ $redirectRecord['source_path'] = '/' . ($sourceDetails['path'] ? $sourceDetails['path'] . '/' : '');
+ }
+
+ // Add the redirect record
+ $connRedirects->insert('sys_redirect', $redirectRecord);
+
+ // Remove the sys_domain record (hard)
+ $connDomains->delete('sys_domain', ['uid' => (int)$domainEntry['uid']]);
+ }
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * parse_url('example.com/bar') returns ['path' => 'example.com/bar'] - it does not
+ * split into 'host' and 'path' if there is no scheme. Adding a scheme in this case
+ * leads to more reliable sys_domain transitions.
+ *
+ * @param string $domainName
+ * @return string[]
+ */
+ protected function getDomainDetails(string $domainName): array
+ {
+ if (substr($domainName, 0, 4) === 'http') {
+ return parse_url($domainName);
+ }
+ return parse_url('https://' . $domainName);
+ }
+}
diff --git a/Classes/Updates/v95/SeparateSysHistoryFromSysLogUpdate.php b/Classes/Updates/v95/SeparateSysHistoryFromSysLogUpdate.php
new file mode 100644
index 0000000..41b1ab8
--- /dev/null
+++ b/Classes/Updates/v95/SeparateSysHistoryFromSysLogUpdate.php
@@ -0,0 +1,399 @@
+checkIfFieldInTableExists('sys_history', 'sys_log_uid')) {
+ return false;
+ }
+
+ // Check if there is data to migrate
+ $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getQueryBuilderForTable('sys_history');
+ $queryBuilder->getRestrictions()->removeAll();
+ $count = $queryBuilder->count('*')
+ ->from('sys_history')
+ ->where($queryBuilder->expr()->neq('sys_log_uid', 0))
+ ->execute()
+ ->fetchColumn(0);
+
+ return $count > 0;
+ }
+
+ /**
+ * @return string[] All new fields and tables must exist
+ */
+ public function getPrerequisites(): array
+ {
+ return [
+ DatabaseUpdatedPrerequisite::class
+ ];
+ }
+
+ /**
+ * Moves data from sys_log into sys_history
+ * where a reference is still there: sys_history.sys_log_uid > 0
+ *
+ * @return bool
+ * @throws \Doctrine\DBAL\ConnectionException
+ * @throws \Exception
+ */
+ public function executeUpdate(): bool
+ {
+ // If rows from the target table that is updated and the sys_registry table are on the
+ // same connection, the update statement and sys_registry position update will be
+ // handled in a transaction to have an atomic operation in case of errors during execution.
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_history');
+ $connectionForSysRegistry = $connectionPool->getConnectionForTable('sys_registry');
+
+ // In case the PHP ended for whatever reason, fetch the last position from registry
+ // and only execute the phase(s) that has/have not been executed yet
+ $startPositionAndPhase = $this->getStartPositionAndPhase();
+
+ if ($startPositionAndPhase['phase'] === self::MOVE_DATA) {
+ $startPositionAndPhase = $this->moveDataFromSysLogToSysHistory(
+ $connection,
+ $connectionForSysRegistry,
+ $startPositionAndPhase
+ );
+ }
+
+ if ($startPositionAndPhase['phase'] === self::UPDATE_HISTORY) {
+ $this->keepHistoryForInsertAndDeleteActions(
+ $connectionForSysRegistry,
+ $startPositionAndPhase
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * @param \TYPO3\CMS\Core\Database\Connection $connection
+ * @param \TYPO3\CMS\Core\Database\Connection $connectionForSysRegistry
+ * @param array $startPositionAndPhase
+ * @return array
+ * @throws \Doctrine\DBAL\ConnectionException
+ * @throws \Exception
+ */
+ protected function moveDataFromSysLogToSysHistory(
+ Connection $connection,
+ Connection $connectionForSysRegistry,
+ array $startPositionAndPhase
+ ): array {
+ do {
+ $processedRows = 0;
+
+ // update "modify" statements (= decoupling)
+ $queryBuilder = $connection->createQueryBuilder();
+ $rows = $queryBuilder->select('sys_history.uid AS history_uid', 'sys_history.history_data', 'sys_log.*')
+ ->from('sys_history')
+ ->leftJoin(
+ 'sys_history',
+ 'sys_log',
+ 'sys_log',
+ $queryBuilder->expr()->eq('sys_history.sys_log_uid', $queryBuilder->quoteIdentifier('sys_log.uid'))
+ )
+ ->where($queryBuilder->expr()->gt('sys_history.uid', $queryBuilder->createNamedParameter($startPositionAndPhase['uid'])))
+ ->setMaxResults(self::BATCH_SIZE)
+ ->orderBy('sys_history.uid', 'ASC')
+ ->execute()
+ ->fetchAll();
+
+ foreach ($rows as $row) {
+ $logData = $this->unserializeToArray((string)($row['log_data'] ?? ''));
+ $historyData = $this->unserializeToArray((string)($row['history_data'] ?? ''));
+ $updateData = [
+ 'actiontype' => RecordHistoryStore::ACTION_MODIFY,
+ 'usertype' => 'BE',
+ 'userid' => $row['userid'],
+ 'sys_log_uid' => 0,
+ 'history_data' => json_encode($historyData),
+ 'originaluserid' => empty($logData['originalUser']) ? null : $logData['originalUser']
+ ];
+
+ if ($connection === $connectionForSysRegistry) {
+ // sys_history and sys_registry tables are on the same connection, use a transaction
+ $connection->beginTransaction();
+ try {
+ $startPositionAndPhase = $this->updateTablesAndTrackProgress(
+ $connection,
+ $connection,
+ $updateData,
+ $logData,
+ $row
+ );
+ $connection->commit();
+ } catch (\Exception $up) {
+ $connection->rollBack();
+ throw ($up);
+ }
+ } else {
+ // Different connections for sys_history and sys_registry -> execute two
+ // distinct queries and hope for the best.
+ $startPositionAndPhase = $this->updateTablesAndTrackProgress(
+ $connection,
+ $connectionForSysRegistry,
+ $updateData,
+ $logData,
+ $row
+ );
+ }
+
+ $processedRows++;
+ }
+ // repeat until a resultset smaller than the batch size was processed
+ } while ($processedRows === self::BATCH_SIZE);
+
+ // phase 0 is finished
+ $registry = GeneralUtility::makeInstance(Registry::class);
+ $startPositionAndPhase = [
+ 'phase' => self::UPDATE_HISTORY,
+ 'uid' => 0,
+ ];
+ $registry->set('installSeparateHistoryFromSysLog', 'phaseAndPosition', $startPositionAndPhase);
+
+ return $startPositionAndPhase;
+ }
+
+ /**
+ * Update sys_history and sys_log tables
+ *
+ * Also keep track of progress in sys_registry
+ *
+ * @param \TYPO3\CMS\Core\Database\Connection $connection
+ * @param \TYPO3\CMS\Core\Database\Connection $connectionForSysRegistry
+ * @param array $updateData
+ * @param array $logData
+ * @param array $row
+ * @return array
+ */
+ protected function updateTablesAndTrackProgress(
+ Connection $connection,
+ Connection $connectionForSysRegistry,
+ array $updateData,
+ array $logData,
+ array $row
+ ): array {
+ $connection->update(
+ 'sys_history',
+ $updateData,
+ ['uid' => (int)$row['history_uid']],
+ ['uid' => Connection::PARAM_INT]
+ );
+
+ // Store information about history entry in sys_log table
+ $logData['history'] = $row['history_uid'];
+ $connection->update(
+ 'sys_log',
+ ['log_data' => serialize($logData)],
+ ['uid' => (int)$row['uid']],
+ ['uid' => Connection::PARAM_INT]
+ );
+ $startPositionAndPhase = [
+ 'phase' => self::MOVE_DATA,
+ 'uid' => $row['history_uid'],
+ ];
+ $connectionForSysRegistry->update(
+ 'sys_registry',
+ [
+ 'entry_value' => serialize($startPositionAndPhase)
+ ],
+ [
+ 'entry_namespace' => 'installSeparateHistoryFromSysLog',
+ 'entry_key' => 'phaseAndPosition',
+ ]
+ );
+
+ return $startPositionAndPhase;
+ }
+
+ /**
+ * Add Insert and Delete actions from sys_log to sys_history
+ *
+ * @param \TYPO3\CMS\Core\Database\Connection $connectionForSysRegistry
+ * @param array $startPositionAndPhase
+ */
+ protected function keepHistoryForInsertAndDeleteActions(
+ Connection $connectionForSysRegistry,
+ array $startPositionAndPhase
+ ) {
+ do {
+ $processedRows = 0;
+
+ // Add insert/delete calls
+ $logQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
+ $result = $logQueryBuilder->select('uid', 'userid', 'action', 'tstamp', 'log_data', 'tablename', 'recuid')
+ ->from('sys_log')
+ ->where(
+ $logQueryBuilder->expr()->eq('type', $logQueryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
+ $logQueryBuilder->expr()->orX(
+ $logQueryBuilder->expr()->eq('action', $logQueryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
+ $logQueryBuilder->expr()->eq('action', $logQueryBuilder->createNamedParameter(3, \PDO::PARAM_INT))
+ )
+ )
+ ->andWhere(
+ $logQueryBuilder->expr()->gt('uid', $logQueryBuilder->createNamedParameter($startPositionAndPhase['uid']))
+ )
+ ->orderBy('uid', 'ASC')
+ ->setMaxResults(self::BATCH_SIZE)
+ ->execute();
+
+ foreach ($result as $row) {
+ $logData = $this->unserializeToArray((string)($row['log_data'] ?? ''));
+ $store = GeneralUtility::makeInstance(
+ RecordHistoryStore::class,
+ RecordHistoryStore::USER_BACKEND,
+ $row['userid'],
+ (empty($logData['originalUser']) ? null : $logData['originalUser']),
+ $row['tstamp']
+ );
+
+ switch ($row['action']) {
+ // Insert
+ case 1:
+ $store->addRecord($row['tablename'], (int)$row['recuid'], $logData);
+ break;
+ // Delete
+ case 3:
+ $store->deleteRecord($row['tablename'], (int)$row['recuid']);
+ break;
+ }
+
+ $startPositionAndPhase = [
+ 'phase' => self::UPDATE_HISTORY,
+ 'uid' => $row['uid'],
+ ];
+ $connectionForSysRegistry->update(
+ 'sys_registry',
+ [
+ 'entry_value' => serialize($startPositionAndPhase)
+ ],
+ [
+ 'entry_namespace' => 'installSeparateHistoryFromSysLog',
+ 'entry_key' => 'phaseAndPosition',
+ ]
+ );
+
+ $processedRows++;
+ }
+ // repeat until a result set smaller than the batch size was processed
+ } while ($processedRows === self::BATCH_SIZE);
+ }
+
+ /**
+ * Checks if given field /column in a table exists
+ *
+ * @param string $table
+ * @param string $fieldName
+ * @return bool
+ */
+ protected function checkIfFieldInTableExists($table, $fieldName): bool
+ {
+ $tableColumns = GeneralUtility::makeInstance(ConnectionPool::class)
+ ->getConnectionForTable($table)
+ ->getSchemaManager()
+ ->listTableColumns($table);
+ return isset($tableColumns[$fieldName]);
+ }
+
+ /**
+ * Returns an array with phase / uid combination that specifies the start position the
+ * update process should start with.
+ *
+ * @return array New start position
+ */
+ protected function getStartPositionAndPhase(): array
+ {
+ $registry = GeneralUtility::makeInstance(Registry::class);
+ $startPosition = $registry->get('installSeparateHistoryFromSysLog', 'phaseAndPosition', []);
+ if (empty($startPosition)) {
+ $startPosition = [
+ 'phase' => self::MOVE_DATA,
+ 'uid' => 0,
+ ];
+ $registry->set('installSeparateHistoryFromSysLog', 'phaseAndPosition', $startPosition);
+ }
+
+ return $startPosition;
+ }
+
+ protected function unserializeToArray(string $serialized): array
+ {
+ $unserialized = unserialize($serialized, ['allowed_classes' => false]);
+ return is_array($unserialized) ? $unserialized : [];
+ }
+}
diff --git a/Classes/Updates/v95/Typo3DbExtractionUpdate.php b/Classes/Updates/v95/Typo3DbExtractionUpdate.php
new file mode 100644
index 0000000..b4d5a41
--- /dev/null
+++ b/Classes/Updates/v95/Typo3DbExtractionUpdate.php
@@ -0,0 +1,116 @@
+extension = new ExtensionModel(
+ 'typo3db_legacy',
+ '$GLOBALS[\'TYPO3_DB\'] compatibility layer',
+ '1.1.1',
+ 'friendsoftypo3/typo3db-legacy',
+ 'This extension provides the well-known database API $GLOBALS[\'TYPO3_DB\'] used in previous TYPO3 versions for extensions that still rely on it.'
+ );
+
+ $this->confirmation = new Confirmation(
+ 'Are you sure?',
+ 'You should install EXT:typo3db_legacy only if you really need it. ' . $this->extension->getDescription(),
+ false
+ );
+ }
+
+ /**
+ * Return a confirmation message instance
+ *
+ * @return \TYPO3\CMS\Install\Updates\Confirmation
+ */
+ public function getConfirmation(): Confirmation
+ {
+ return $this->confirmation;
+ }
+
+ /**
+ * Return the identifier for this wizard
+ * This should be the same string as used in the ext_localconf class registration
+ *
+ * @return string
+ */
+ public function getIdentifier(): string
+ {
+ return 'typo3DbLegacyExtension';
+ }
+
+ /**
+ * Return the speaking name of this wizard
+ *
+ * @return string
+ */
+ public function getTitle(): string
+ {
+ return 'Install extension "typo3db_legacy" from TER';
+ }
+
+ /**
+ * Return the description for this wizard
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return 'The old database API populated as $GLOBALS[\'TYPO3_DB\'] has been extracted into'
+ . ' the TYPO3 Extension Repository. This update downloads the TYPO3 extension typo3db_legacy from the TER.'
+ . ' Use this if you\'re dealing with extensions in the instance that still rely on the old database API.';
+ }
+
+ /**
+ * Is an update necessary?
+ * Is used to determine whether a wizard needs to be run.
+ *
+ * @return bool
+ */
+ public function updateNecessary(): bool
+ {
+ return !ExtensionManagementUtility::isLoaded($this->extension->getKey());
+ }
+
+ /**
+ * Returns an array of class names of Prerequisite classes
+ * This way a wizard can define dependencies like "database up-to-date" or
+ * "reference index updated"
+ *
+ * @return string[]
+ */
+ public function getPrerequisites(): array
+ {
+ return [];
+ }
+}
diff --git a/Configuration/Commands.php b/Configuration/Commands.php
deleted file mode 100644
index 9b0bae5..0000000
--- a/Configuration/Commands.php
+++ /dev/null
@@ -1,15 +0,0 @@
- [
- 'class' => \IchHabRecht\Upgrader\Command\UpgradeCommand::class,
- 'runLevel' => \Helhum\Typo3Console\Core\Booting\RunLevel::LEVEL_COMPILE,
- 'schedulable' => false,
- ],
-];
diff --git a/Configuration/Services.php b/Configuration/Services.php
new file mode 100644
index 0000000..d5b5880
--- /dev/null
+++ b/Configuration/Services.php
@@ -0,0 +1,18 @@
+services();
+
+ if (class_exists(\Helhum\Typo3Console\Core\Booting\RunLevel::class)) {
+ $services->set(\IchHabRecht\Upgrader\Command\UpgradeCommand::class)
+ ->tag('console.command', [
+ 'command' => 'coreupgrader:upgrade',
+ 'runLevel' => \Helhum\Typo3Console\Core\Booting\RunLevel::LEVEL_COMPILE,
+ 'schedulable' => false,
+ ]);
+ }
+};
diff --git a/Configuration/Upgrades.php b/Configuration/Upgrades.php
index 5afd659..c164784 100644
--- a/Configuration/Upgrades.php
+++ b/Configuration/Upgrades.php
@@ -55,86 +55,77 @@
],
'v9.5' => [
'typo3DbLegacyExtension' => [
- 'typo3DbLegacyExtension' => \TYPO3\CMS\Install\Updates\Typo3DbExtractionUpdate::class,
+ 'typo3DbLegacyExtension' => \TYPO3\CMS\v95\Install\Updates\Typo3DbExtractionUpdate::class,
],
'funcExtension' => [
- 'funcExtension' => \TYPO3\CMS\Install\Updates\FuncExtractionUpdate::class,
+ 'funcExtension' => \TYPO3\CMS\v95\Install\Updates\FuncExtractionUpdate::class,
],
'pagesUrltypeField' => [
- 'pagesUrltypeField' => \TYPO3\CMS\Install\Updates\MigrateUrlTypesInPagesUpdate::class,
+ 'pagesUrltypeField' => \TYPO3\CMS\v95\Install\Updates\MigrateUrlTypesInPagesUpdate::class,
],
'separateSysHistoryFromLog' => [
- 'separateSysHistoryFromLog' => \TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate::class,
+ 'separateSysHistoryFromLog' => \TYPO3\CMS\v95\Install\Updates\SeparateSysHistoryFromSysLogUpdate::class,
],
'rdctExtension' => [
- 'rdctExtension' => \TYPO3\CMS\Install\Updates\RedirectExtractionUpdate::class,
+ 'rdctExtension' => \TYPO3\CMS\v95\Install\Updates\RedirectExtractionUpdate::class,
],
'pagesLanguageOverlay' => [
- 'pagesLanguageOverlay' => \TYPO3\CMS\Install\Updates\MigratePagesLanguageOverlayUpdate::class,
+ 'pagesLanguageOverlay' => \TYPO3\CMS\v95\Install\Updates\MigratePagesLanguageOverlayUpdate::class,
],
'pagesLanguageOverlayBeGroupsAccessRights' => [
- 'pagesLanguageOverlayBeGroupsAccessRights' => \TYPO3\CMS\Install\Updates\MigratePagesLanguageOverlayBeGroupsAccessRights::class,
+ 'pagesLanguageOverlayBeGroupsAccessRights' => \TYPO3\CMS\v95\Install\Updates\MigratePagesLanguageOverlayBeGroupsAccessRights::class,
],
'backendLayoutIcons' => [
- 'backendLayoutIcons' => \TYPO3\CMS\Install\Updates\BackendLayoutIconUpdateWizard::class,
+ 'backendLayoutIcons' => \TYPO3\CMS\v95\Install\Updates\BackendLayoutIconUpdateWizard::class,
],
'redirects' => [
- 'redirects' => \TYPO3\CMS\Install\Updates\RedirectsExtensionUpdate::class,
+ 'redirects' => \TYPO3\CMS\v95\Install\Updates\RedirectsExtensionUpdate::class,
],
'adminpanelExtension' => [
- 'adminpanelExtension' => \TYPO3\CMS\Install\Updates\AdminPanelInstall::class,
+ 'adminpanelExtension' => \TYPO3\CMS\v95\Install\Updates\AdminPanelInstall::class,
],
'pagesSlugs' => [
- 'pagesSlugs' => \TYPO3\CMS\Install\Updates\PopulatePageSlugs::class,
+ 'pagesSlugs' => \TYPO3\CMS\v95\Install\Updates\PopulatePageSlugs::class,
],
'argon2iPasswordHashes' => [
- 'argon2iPasswordHashes' => \TYPO3\CMS\Install\Updates\Argon2iPasswordHashes::class,
+ 'argon2iPasswordHashes' => \TYPO3\CMS\v95\Install\Updates\Argon2iPasswordHashes::class,
],
'backendUsersConfiguration' => [
- 'backendUsersConfiguration' => \TYPO3\CMS\Install\Updates\BackendUserConfigurationUpdate::class,
+ 'backendUsersConfiguration' => \TYPO3\CMS\v95\Install\Updates\BackendUserConfigurationUpdate::class,
],
],
'v10.4' => [
'rsaauthExtension' => [
- 'rsaauthExtension' => \TYPO3\CMS\Install\Updates\RsaauthExtractionUpdate::class,
+ 'rsaauthExtension' => \TYPO3\CMS\v104\Install\Updates\RsaauthExtractionUpdate::class,
],
'feeditExtension' => [
- 'feeditExtension' => \TYPO3\CMS\Install\Updates\FeeditExtractionUpdate::class,
+ 'feeditExtension' => \TYPO3\CMS\v104\Install\Updates\FeeditExtractionUpdate::class,
],
'taskcenterExtension' => [
- 'taskcenterExtension' => \TYPO3\CMS\Install\Updates\TaskcenterExtractionUpdate::class,
+ 'taskcenterExtension' => \TYPO3\CMS\v104\Install\Updates\TaskcenterExtractionUpdate::class,
],
'sysActionExtension' => [
- 'sysActionExtension' => \TYPO3\CMS\Install\Updates\SysActionExtractionUpdate::class,
+ 'sysActionExtension' => \TYPO3\CMS\v104\Install\Updates\SysActionExtractionUpdate::class,
],
'databaseRowsUpdateWizard' => [
- 'databaseRowsUpdateWizard' => \TYPO3\CMS\Install\Updates\DatabaseRowsUpdateWizard::class,
+ 'databaseRowsUpdateWizard' => \TYPO3\CMS\v104\Install\Updates\DatabaseRowsUpdateWizard::class,
],
],
];
-$additionalUpgrades = [
- 'v10.4' => [
- 'TYPO3\\CMS\\Felogin\\Updates\\MigrateFeloginPlugins' => [
- 'TYPO3\\CMS\\Felogin\\Updates\\MigrateFeloginPlugins' => TYPO3\CMS\Felogin\Updates\MigrateFeloginPlugins::class,
- ],
- 'TYPO3\\CMS\\FrontendLogin\\Updates\\MigrateFeloginPluginsCtype' => [
- 'TYPO3\\CMS\\FrontendLogin\\Updates\\MigrateFeloginPluginsCtype' => TYPO3\CMS\FrontendLogin\Updates\MigrateFeloginPluginsCtype::class,
- ],
- 'formFileExtension' => [
- 'formFileExtension' => \TYPO3\CMS\Form\Hooks\FormFileExtensionUpdate::class,
- ],
- ],
-];
+if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('felogin')) {
+ $upgrades['v10.4']['TYPO3\\CMS\\FrontendLogin\\Updates\\MigrateFeloginPlugins'] = [
+ 'TYPO3\\CMS\\FrontendLogin\\Updates\\MigrateFeloginPlugins' => \TYPO3\CMS\v104\Install\Updates\MigrateFeloginPlugins::class,
+ ];
+ $upgrades['v10.4']['TYPO3\\CMS\\FrontendLogin\\Updates\\MigrateFeloginPluginsCtype'] = [
+ 'TYPO3\\CMS\\FrontendLogin\\Updates\\MigrateFeloginPluginsCtype' => \TYPO3\CMS\v104\Install\Updates\MigrateFeloginPluginsCtype::class,
+ ];
+}
-foreach ($additionalUpgrades as $version => $versionUpgrades) {
- foreach ($versionUpgrades as $key => $upgradeArray) {
- foreach ($upgradeArray as $identifier => $class) {
- if (class_exists($class)) {
- $upgrades[$version][$key] = $upgradeArray;
- }
- }
- }
+if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('form')) {
+ $upgrades['v10.4']['formFileExtension'] = [
+ 'formFileExtension' => \TYPO3\CMS\v104\Install\Updates\FormFileExtensionUpdate::class,
+ ];
}
return $upgrades;
diff --git a/README.md b/README.md
index 492e115..5fff90f 100644
--- a/README.md
+++ b/README.md
@@ -4,11 +4,11 @@
[](https://travis-ci.org/IchHabRecht/core_upgrader)
[](https://styleci.io/repos/263364343)
-Run upgrade wizards for multiple TYPO3 versions (to 10.4) at once.
+Run upgrade wizards for multiple TYPO3 versions (to 11.5) at once.
## Features
-This extension allows to upgrade the TYPO3 core from v7.6 to v10.4 in one step.
+This extension allows to upgrade the TYPO3 core from v7.6 to v11.5 in one step.
## Installation
@@ -21,18 +21,14 @@ The typo3cms binary will be installed in the specified bin-dir (by default `vend
## Usage
-1. You need to activate the extension:
+1. Now you can run all update wizards:
- `typo3cms install:generatepackagestates`
+ `typo3cms coreupgrader:upgrade`
-2. Now you can run all update wizards:
+2. The upgrade command runs necessary TYPO3 upgrade wizards.\
+ It is recommended to run TYPO3 Console upgrade command afterwards to execute confirmable and extension wizards.
- `typo3cms coreupgrader:upgrade`
-
- 3. The upgrade command runs necessary TYPO3 upgrade wizards.\
- It is recommended to run TYPO3 Console upgrade command afterwards to execute confirmable and extension wizards.
-
- `typo3cms upgrade:run all`
+ `typo3cms upgrade:run all`
## Community
diff --git a/composer.json b/composer.json
index 734af89..1f77a61 100644
--- a/composer.json
+++ b/composer.json
@@ -19,16 +19,18 @@
}
],
"require": {
- "php": ">=7.2, < 7.5",
- "typo3/cms-core": "^10.4",
- "typo3/cms-frontend": "^10.4",
- "typo3/cms-install": "^10.4",
- "helhum/typo3-console": "^6.3.0"
+ "php": ">=7.2 || ^8.0",
+ "typo3/cms-core": "^11.5.0",
+ "typo3/cms-frontend": "^11.5.0",
+ "typo3/cms-install": "^11.5.0",
+ "helhum/typo3-console": "^7.0.0"
},
"autoload": {
"psr-4": {
"IchHabRecht\\Upgrader\\": "Classes/",
- "TYPO3\\CMS\\v87\\Install\\Updates\\": "Classes/Updates/v87/"
+ "TYPO3\\CMS\\v87\\Install\\Updates\\": "Classes/Updates/v87/",
+ "TYPO3\\CMS\\v95\\Install\\Updates\\": "Classes/Updates/v95/",
+ "TYPO3\\CMS\\v104\\Install\\Updates\\": "Classes/Updates/v104/"
}
},
"replace": {
diff --git a/ext_tables.sql b/ext_tables.sql
new file mode 100644
index 0000000..0045bfb
--- /dev/null
+++ b/ext_tables.sql
@@ -0,0 +1,7 @@
+#
+# Table structure for table 'pages'
+#
+CREATE TABLE pages (
+ # @deprecated since v9 and will be removed in TYPO3 v11. Legacy connection UID field to pages_language_overlay table
+ legacy_overlay_uid int(11) unsigned DEFAULT '0' NOT NULL,
+);