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 @@ [![Build Status](https://img.shields.io/travis/IchHabRecht/core_upgrader/master.svg)](https://travis-ci.org/IchHabRecht/core_upgrader) [![StyleCI](https://styleci.io/repos/263364343/shield?branch=master)](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, +);