first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
commit 1218dd1517
12716 files changed
+989233

No files matched your search

@@ -0,0 +1,183 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extensionmanager\Utility\Connection\TerUtility;
use TYPO3\CMS\Extensionmanager\Utility\FileHandlingUtility;
use TYPO3\CMS\Extensionmanager\Utility\InstallUtility;
use TYPO3\CMS\Extensionmanager\Utility\ListUtility;
/**
* Download extension from TER
*/
abstract class AbstractDownloadExtensionUpdate implements UpgradeWizardInterface, ConfirmableInterface, ChattyInterface
{
/**
* @var string
*/
protected $repositoryUrl = 'https://typo3.org/fileadmin/ter/@filename';
/**
* @var OutputInterface
*/
protected $output;
/**
* @var \TYPO3\CMS\Install\Updates\ExtensionModel
*/
protected $extension;
/**
* @param OutputInterface $output
*/
public function setOutput(OutputInterface $output): void
{
$this->output = $output;
}
/**
* Execute the update
* Called when a wizard reports that an update is necessary
*
* @return bool
*/
public function executeUpdate(): bool
{
return $this->installExtension($this->extension);
}
/**
* This method can be called to install an extension following all proper processes
* (e.g. installing in extList, respecting priority, etc.)
*
* @param \TYPO3\CMS\Install\Updates\ExtensionModel $extension
* @return bool whether the installation worked or not
* @throws \TYPO3\CMS\Extensionmanager\Exception\ExtensionManagerException
*/
protected function installExtension(ExtensionModel $extension): bool
{
$updateSuccessful = true;
/** @var ObjectManager $objectManager */
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
/** @var ListUtility $extensionListUtility */
$extensionListUtility = $objectManager->get(ListUtility::class);
$availableExtensions = $extensionListUtility->getAvailableExtensions();
$extensionKey = $extension->getKey();
$isExtensionAvailable = !empty($availableExtensions[$extensionKey]);
$isComposerMode = Environment::isComposerMode();
if (!$isComposerMode && !$isExtensionAvailable) {
/** @var TerUtility $extensionTerUtility */
$extensionTerUtility = $objectManager->get(TerUtility::class);
$t3xContent = $this->fetchExtension($extensionKey, $extension->getVersionString());
if (empty($t3xContent)) {
$updateSuccessful = false;
$this->output->writeln('<error>The extension ' . $extensionKey . ' could not be downloaded.</error>');
}
$t3xExtracted = $extensionTerUtility->decodeExchangeData($t3xContent);
if (empty($t3xExtracted) || !is_array($t3xExtracted) || empty($t3xExtracted['extKey'])) {
$updateSuccessful = false;
$this->output->writeln('<error>The extension ' . $extensionKey . ' could not be extracted.</error>');
}
/** @var FileHandlingUtility $extensionFileHandlingUtility */
$extensionFileHandlingUtility = $objectManager->get(FileHandlingUtility::class);
$extensionFileHandlingUtility->unpackExtensionFromExtensionDataArray($t3xExtracted);
// The listUtility now needs to have the regenerated list of packages
$extensionListUtility->reloadAvailableExtensions();
}
if ($isComposerMode && !$isExtensionAvailable) {
$updateSuccessful = false;
$this->output->writeln(
'<warning>The extension ' . $extensionKey
. ' can not be downloaded since Composer is used for package management. Please require this '
. 'extension as package via Composer: "composer require ' . $extension->getComposerName()
. ':^' . $extension->getVersionString() . '"</warning>'
);
}
if ($updateSuccessful) {
/** @var InstallUtility $extensionInstallUtility */
$extensionInstallUtility = $objectManager->get(InstallUtility::class);
$extensionInstallUtility->install($extensionKey);
}
return $updateSuccessful;
}
/**
* Fetch extension from repository
*
* @param string $extensionKey The extension key to fetch
* @param string $version The version to fetch
* @throws \InvalidArgumentException
* @return string T3X file content
*/
protected function fetchExtension($extensionKey, $version): string
{
if (empty($extensionKey) || empty($version)) {
throw new \InvalidArgumentException(
'No extension key for fetching an extension was given.',
1344687432
);
}
$filename = $extensionKey[0] . '/' . $extensionKey[1] . '/' . $extensionKey . '_' . $version . '.t3x';
$url = str_replace('@filename', $filename, $this->repositoryUrl);
return $this->fetchUrl($url);
}
/**
* Open an URL and return the response
* This wrapper method is required to try several download methods if
* the configuration is not valid or initially written by the installer.
*
* @param string $url The URL to file
* @throws \Exception
* @throws \InvalidArgumentException
* @return string File content
*/
protected function fetchUrl($url): string
{
if (empty($url)) {
throw new \InvalidArgumentException(
'No URL for downloading an extension given.',
1344687436
);
}
$fileContent = GeneralUtility::getUrl($url);
// Can not fetch url, throw an exception
if ($fileContent === false) {
throw new \RuntimeException(
'Can not fetch URL "' . $url . '". Possible reasons are network problems or misconfiguration.',
1344685036
);
}
return $fileContent;
}
}
@@ -0,0 +1,309 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Generic class that every update wizard class inherits from.
* Used by the update wizard in the install tool.
*
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
*/
abstract class AbstractUpdate implements UpgradeWizardInterface, ChattyInterface
{
public function __construct()
{
trigger_error('Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this), E_USER_DEPRECATED);
}
/**
* The human-readable title of the upgrade wizard
*
* @var string
*/
protected $title;
/**
* The update wizard identifier
*
* @var string
*/
protected $identifier;
/**
* User input, set from outside
*
* @var string
*/
public $userInput;
/**
* @var OutputInterface
*/
protected $output;
/**
* Returns the title attribute
*
* @deprecated Deprecated since TYPO3 v9
* @return string The title of this update wizard
*/
public function getTitle(): string
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
if ($this->title) {
return $this->title;
}
return $this->identifier;
}
/**
* Sets the title attribute
*
* @param string $title The title of this update wizard
*/
public function setTitle($title)
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
$this->title = $title;
}
/**
* Returns the identifier of this class
*
* @return string The identifier of this update wizard
*/
public function getIdentifier(): string
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
return $this->identifier ?? static::class;
}
/**
* Sets the identifier attribute
*
* @param string $identifier The identifier of this update wizard
*/
public function setIdentifier($identifier)
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
$this->identifier = $identifier;
}
/**
* Simple wrapper function that helps dealing with the compatibility
* layer that some update wizards don't have a second parameter
* thus, it evaluates everything already
*
* @return bool If the wizard should be shown at all on the overview page
* @see checkForUpdate()
*/
public function shouldRenderWizard()
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
$explanation = '';
$result = $this->checkForUpdate($explanation);
return (bool)$result === true;
}
/**
* Check if given table exists
*
* @param string $table
* @return bool
*/
protected function checkIfTableExists($table)
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
$tableExists = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable($table)
->getSchemaManager()
->tablesExist([$table]);
return $tableExists;
}
/**
* Checks whether updates are required.
*
* @param string &$description The description for the update
* @return bool Whether an update is required (TRUE) or not (FALSE)
*/
abstract public function checkForUpdate(&$description);
/**
* Performs the accordant updates.
*
* @param array &$dbQueries Queries done in this update
* @param string &$customMessage Custom message
* @return bool Whether everything went smoothly or not
*/
abstract public function performUpdate(array &$dbQueries, &$customMessage);
/**
* This method can be called to install extensions following all proper processes
* (e.g. installing in extList, respecting priority, etc.)
*
* @param array $extensionKeys List of keys of extensions to install
*/
protected function installExtensions(array $extensionKeys)
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
/** @var \TYPO3\CMS\Extensionmanager\Utility\InstallUtility $installUtility */
$installUtility = GeneralUtility::makeInstance(
\TYPO3\CMS\Extensionmanager\Utility\InstallUtility::class
);
$installUtility->install($extensionKeys);
}
/**
* Marks some wizard as being "seen" so that it not shown again.
*
* Writes the info in LocalConfiguration.php
*
* @param mixed $confValue The configuration is set to this value
*/
protected function markWizardAsDone($confValue = 1)
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
GeneralUtility::makeInstance(Registry::class)->set('installUpdate', static::class, $confValue);
}
/**
* Checks if this wizard has been "done" before
*
* @return bool TRUE if wizard has been done before, FALSE otherwise
*/
protected function isWizardDone()
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
$wizardClassName = static::class;
return GeneralUtility::makeInstance(Registry::class)->get('installUpdate', $wizardClassName, false);
}
/**
* Return the description for this wizard
*
* @return string
*/
public function getDescription(): string
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
return '';
}
/**
* Execute the update
* Called when a wizard reports that an update is necessary
*
* @return bool
*/
public function executeUpdate(): bool
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
$queries = [];
$message = '';
$result = $this->performUpdate($queries, $message);
$this->output->write($message);
return $result;
}
/**
* Is an update necessary?
* Is used to determine whether a wizard needs to be run.
* Check if data for migration exists.
*
* @return bool
*/
public function updateNecessary(): bool
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
$description = '';
$result = $this->checkForUpdate($description);
$this->output->write($description);
return $result;
}
/**
* 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
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Setter injection for output into upgrade wizards
*
* @param OutputInterface $output
*/
public function setOutput(OutputInterface $output): void
{
trigger_error(
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use UpgradeWizardInterface directly. affected class: ' . get_class($this),
E_USER_DEPRECATED
);
$this->output = $output;
}
}
@@ -0,0 +1,113 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
/**
* Installs EXT:adminpanel
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class AdminPanelInstall extends AbstractDownloadExtensionUpdate
{
/**
* @var \TYPO3\CMS\Install\Updates\Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->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 [];
}
}
@@ -0,0 +1,116 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Crypto\PasswordHashing\Argon2iPasswordHash;
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Informational upgrade wizard to remind upgrading instances
* may have to verify argon2i is available on the live servers
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class Argon2iPasswordHashes implements UpgradeWizardInterface, ConfirmableInterface
{
protected $confirmation;
public function __construct()
{
$this->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 \TYPO3\CMS\Install\Updates\Confirmation
*/
public function getConfirmation(): Confirmation
{
return $this->confirmation;
}
}
@@ -0,0 +1,308 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/**
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Doctrine\DBAL\DBALException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Upgrade wizard which goes through all files referenced in backend_layout.icon
* and creates sys_file records as well as sys_file_reference records for each hit.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class BackendLayoutIconUpdateWizard implements UpgradeWizardInterface, ChattyInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @var OutputInterface
*/
protected $output;
/**
* @var ResourceStorage
*/
protected $storage;
/**
* Table to migrate records from
*
* @var string
*/
protected $table = 'backend_layout';
/**
* Table field holding the migration to be
*
* @var string
*/
protected $fieldToMigrate = 'icon';
/**
* the source file resides here
*
* @var string
*/
protected $sourcePath = 'uploads/media/';
/**
* target folder after migration
* Relative to fileadmin
*
* @var string
*/
protected $targetPath = '_migrated/backend_layouts/';
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'backendLayoutIcons';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate all file relations from backend_layout.icon to sys_file_references';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'This update wizard goes through all files that are referenced in the'
. ' backend_layout.icon field and adds the files to the FAL File Index.'
. ' It also moves the files from uploads/ to the fileadmin/_migrated/ path.';
}
/**
* @return bool True if there are records to update
*/
public function updateNecessary(): bool
{
return !empty($this->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' => $i + 256,
'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();
}
}
}
@@ -0,0 +1,150 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Update the backend user "uc" array to use arrays for its structure, as old TYPO3 versions sometimes used stdClasses
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class BackendUserConfigurationUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'backendUsersConfiguration';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Update backend user configuration array';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'The backend user "uc" array, which is persisted in the db, now only allows for'
. ' arrays inside its structure instead of stdClass objects.'
. ' Update the uc structure for all backend users.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$needsExecution = false;
foreach ($this->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']]);
}
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]);
}
}
@@ -0,0 +1,128 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Update backend user setting startModule if set to "help_aboutmodules" or "help_CshmanualCshmanual"
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class BackendUserStartModuleUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'cshmanualBackendUsers';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Update backend user setting "startModule"';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'The backend user setting startModule is changed for the extensions about/aboutmodules'
. ' and help/cshmanual. Update all backend users that use EXT:aboutmodules and'
. ' EXT:cshmanual as startModule.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$statement = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('be_users')
->select(['uid', 'uc'], 'be_users', []);
$needsExecution = false;
while ($backendUser = $statement->fetch()) {
if ($backendUser['uc'] !== null) {
$userConfig = unserialize($backendUser['uc'], ['allowed_classes' => false]);
if ($userConfig['startModule'] === 'help_aboutmodules'
|| $userConfig['startModule'] === 'help_AboutmodulesAboutmodules'
|| $userConfig['startModule'] === 'help_AboutAboutmodules'
|| $userConfig['startModule'] === 'help_CshmanualCshmanual'
|| $userConfig['startModule'] === 'help_DocumentationCshmanual'
) {
$needsExecution = true;
break;
}
}
}
return $needsExecution;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update if backend user's startmodule is
* "help_aboutmodules" or "help_AboutmodulesAboutmodules" or "help_CshmanualCshmanual"
*
* @return bool
*/
public function executeUpdate(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
$statement = $queryBuilder->select('uid', 'uc')->from('be_users')->execute();
while ($backendUser = $statement->fetch()) {
if ($backendUser['uc'] !== null) {
$userConfig = unserialize($backendUser['uc'], ['allowed_classes' => false]);
if ($userConfig['startModule'] === 'help_aboutmodules'
|| $userConfig['startModule'] === 'help_AboutmodulesAboutmodules'
|| $userConfig['startModule'] === 'help_AboutAboutmodules'
|| $userConfig['startModule'] === 'help_CshmanualCshmanual'
|| $userConfig['startModule'] === 'help_DocumentationCshmanual'
) {
$userConfig['startModule'] = 'help_AboutAbout';
if ($userConfig['startModule'] === 'help_CshmanualCshmanual' || $userConfig['startModule'] === 'help_DocumentationCshmanual') {
$userConfig['startModule'] = 'help_BackendCshmanual';
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
$queryBuilder->update('be_users')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($backendUser['uid'], \PDO::PARAM_INT)
)
)
->set('uc', $queryBuilder->createNamedParameter(serialize($userConfig), \PDO::PARAM_LOB))
->execute();
}
}
}
return true;
}
}
@@ -0,0 +1,112 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Migrate bullet content element rendering from layout to bullets_type
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class BulletContentElementUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'bulletContentElementUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate bullet content element rendering selector from layout to bullets_type';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Rendering type field has been streamlined with fluid_styled_content.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$elementCount = $queryBuilder->count('uid')
->from('tt_content')
->where(
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('bullets', \PDO::PARAM_STR)),
$queryBuilder->expr()->in('layout', [1, 2])
)
->execute()
->fetchColumn(0);
return (bool)$elementCount;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update
*
* @return bool
*/
public function executeUpdate(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder->select('uid', 'layout')
->from('tt_content')
->where(
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('bullets', \PDO::PARAM_STR)),
$queryBuilder->expr()->in('layout', [1, 2])
)
->execute();
while ($record = $statement->fetch()) {
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->update('tt_content')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
)
)
->set('layout', 0, false)
->set('bullets_type', $record['layout']);
$queryBuilder->execute();
}
return true;
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\Console\Output\OutputInterface;
/**
* Is this upgradeWizard chatty aka does it need to output things?
*/
interface ChattyInterface
{
/**
* Setter injection for output into upgrade wizards
*
* @param OutputInterface $output
*/
public function setOutput(OutputInterface $output): void;
}
@@ -0,0 +1,179 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Remove all backend users starting with _cli_
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class CommandLineBackendUserRemovalUpdate implements UpgradeWizardInterface, ChattyInterface, RepeatableInterface, ConfirmableInterface
{
/**
* @var OutputInterface
*/
protected $output;
/**
* @var Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->confirmation = new Confirmation(
'Are you sure?',
'The following backend users will be removed: ' . implode(', ', $this->getUnneededCommandLineUsers()),
true
);
}
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'commandLineBackendUserRemovalUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Remove unneeded CLI backend users';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'The command line interface does not need to have custom _cli_* backend users anymore.'
. ' They can safely be deleted.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$needsExecution = false;
$usersFound = $this->getUnneededCommandLineUsers();
if (!empty($usersFound)) {
$needsExecution = true;
}
return $needsExecution;
}
/**
* @param OutputInterface $output
*/
public function setOutput(OutputInterface $output): void
{
$this->output = $output;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class,
];
}
/**
* Performs the database update to set all be_users starting with _CLI_* to deleted
*
* @return bool
*/
public function executeUpdate(): bool
{
$usersFound = $this->getUnneededCommandLineUsers();
foreach ($usersFound as $userUid => $username) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
$queryBuilder->update('be_users')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($userUid, \PDO::PARAM_INT)
)
)
// "false" is set as third parameter to have the final
// value in $databaseQueries and not a statement placeholder
->set('deleted', 1, false)
->execute();
}
$this->output->writeln('The following backend users have been deleted:');
foreach ($usersFound as $user) {
$this->output->writeln('* ' . $user);
}
return true;
}
/**
* Find all backend users starting with _CLI_ that are not deleted yet.
*
* @return array a list of uids
*/
protected function getUnneededCommandLineUsers(): array
{
$commandLineUsers = [];
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('be_users');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder
->select('uid', 'username')
->from('be_users')
->where(
// Using query builder is complicated in this case. Get it straight, no user input is involved.
'LOWER(username) LIKE \'_cli_%\'',
$queryBuilder->expr()->neq(
'username',
$queryBuilder->createNamedParameter('_cli_', \PDO::PARAM_STR)
)
)
->execute();
while ($row = $result->fetch()) {
$commandLineUsers[$row['uid']] = $row['username'];
}
return $commandLineUsers;
}
/**
* Return a confirmation message instance
*
* @return Confirmation
*/
public function getConfirmation(): Confirmation
{
return $this->confirmation;
}
}
@@ -0,0 +1,138 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
/**
* Installs and downloads EXT:compatibility7 if needed
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class Compatibility7ExtractionUpdate extends AbstractDownloadExtensionUpdate
{
/**
* @var string
*/
protected $title = 'Install extension "compatibility7" from TER';
/**
* @var string
*/
protected $extensionKey = 'compatibility7';
/**
* @var array
*/
protected $extensionDetails = [
'compatibility7' => [
'title' => 'Compatibility Mode for TYPO3 v7',
'description' => 'Provides an additional backwards-compatibility layer with legacy functionality for sites that haven\'t fully migrated to TYPO3 v8 yet.',
'versionString' => '8.7.1',
'composerName' => 'friendsoftypo3/compatibility7',
],
];
/**
* @var \TYPO3\CMS\Install\Updates\ExtensionModel
*/
protected $extension;
/**
* @var \TYPO3\CMS\Install\Updates\Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->extension = new ExtensionModel(
'compatibility7',
'Compatibility Mode for TYPO3 v7',
'8.7.1',
'friendsoftypo3/compatibility7',
'Provides an additional backwards-compatibility layer with legacy functionality for sites that haven\'t fully migrated to TYPO3 v8 yet.'
);
$this->confirmation = new Confirmation(
'Are you sure?',
'The compatibility extensions come with a performance penalty, use 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 'compatibility7Extension';
}
/**
* Return the speaking name of this wizard
*
* @return string
*/
public function getTitle(): string
{
return 'Install compatibility extension for TYPO3 7 compatibility';
}
/**
* Return the description for this wizard
*
* @return string
*/
public function getDescription(): string
{
return 'The extension "compatibility7" (Compatibility Mode for TYPO3 v7) was extracted into '
. 'the TYPO3 Extension Repository. This update downloads the TYPO3 Extension from the TER.';
}
/**
* Is an update necessary?
* Is used to determine whether a wizard needs to be run.
*
* @return bool
*/
public function updateNecessary(): bool
{
return !ExtensionManagementUtility::isLoaded('compatibility7');
}
/**
* 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 [];
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Use if upgrade wizard needs confirmation
*/
interface ConfirmableInterface
{
/**
* Return a confirmation message instance
*
* @return \TYPO3\CMS\Install\Updates\Confirmation
*/
public function getConfirmation(): Confirmation;
}
@@ -0,0 +1,122 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
class Confirmation
{
/**
* @var bool
*/
protected $defaultValue = false;
/**
* @var string
*/
protected $title = '';
/**
* @var string
*/
protected $message = '';
/**
* @var string
*/
protected $confirm;
/**
* @var string
*/
protected $deny;
/**
* @var bool
*/
protected $required;
/**
* @param string $title
* @param string $message
* @param bool $defaultValue
* @param string $confirm
* @param string $deny
* @param bool $required
*/
public function __construct(
string $title,
string $message,
bool $defaultValue = false,
string $confirm = 'Yes, execute',
string $deny = 'No, do not execute',
bool $required = false
) {
$this->title = $title;
$this->message = $message;
$this->defaultValue = $defaultValue;
$this->confirm = $confirm;
$this->deny = $deny;
$this->required = $required;
}
/**
* @return string
*/
public function getConfirm(): string
{
return $this->confirm;
}
/**
* @return string
*/
public function getDeny(): string
{
return $this->deny;
}
/**
* @return bool
*/
public function isRequired(): bool
{
return $this->required;
}
/**
* @return bool
*/
public function getDefaultValue(): bool
{
return $this->defaultValue;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @return string
*/
public function getMessage(): string
{
return $this->message;
}
}
@@ -0,0 +1,328 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Updates\RowUpdater\ImageCropUpdater;
use TYPO3\CMS\Install\Updates\RowUpdater\L10nModeUpdater;
use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
use TYPO3\CMS\Install\Updates\RowUpdater\RteLinkSyntaxUpdater;
/**
* This is a generic updater to migrate content of TCA rows.
*
* Multiple classes implementing interface "RowUpdateInterface" can be
* registered here, each for a specific update purpose.
*
* The updater fetches each row of all TCA registered tables and
* visits the client classes who may modify the row content.
*
* The updater remembers for each class if it run through, so the updater
* will be shown again if a new updater class is registered that has not
* been run yet.
*
* A start position pointer is stored in the registry that is updated during
* the run process, so if for instance the PHP process runs into a timeout,
* the job can restart at the position it stopped.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class DatabaseRowsUpdateWizard implements UpgradeWizardInterface, RepeatableInterface
{
/**
* @var array Single classes that may update rows
*/
protected $rowUpdater = [
L10nModeUpdater::class,
ImageCropUpdater::class,
RteLinkSyntaxUpdater::class,
];
/**
* @internal
* @return string[]
*/
public function getAvailableRowUpdater(): array
{
return $this->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.
$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) {
// Target table and sys_registry table are on the same connection, use a transaction
$connectionForTable->beginTransaction();
try {
$connectionForTable->update(
$table,
$updatedFields,
[
'uid' => $rowBefore['uid'],
]
);
$connectionForTable->update(
'sys_registry',
[
'entry_value' => serialize($startPosition),
],
[
'entry_namespace' => 'installUpdateRows',
'entry_key' => 'rowUpdatePosition',
]
);
$connectionForTable->commit();
} catch (\Exception $up) {
$connectionForTable->rollBack();
throw $up;
}
} else {
// Different connections for table and sys_registry -> execute two
// distinct queries and hope for the best.
$connectionForTable->update(
$table,
$updatedFields,
[
'uid' => $rowBefore['uid'],
]
);
$connectionForSysRegistry->update(
'sys_registry',
[
'entry_value' => serialize($startPosition),
],
[
'entry_namespace' => 'installUpdateRows',
'entry_key' => 'rowUpdatePosition',
]
);
}
}
}
}
// 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;
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Install\Service\UpgradeWizardsService;
/**
* Prerequisite for upgrade wizards to ensure the database is up-to-date
*
* @internal
*/
class DatabaseUpdatedPrerequisite implements PrerequisiteInterface, ChattyInterface
{
/**
* @var UpgradeWizardsService
*/
protected $upgradeWizardsService;
/**
* @var OutputInterface
*/
protected $output;
public function __construct()
{
$this->upgradeWizardsService = new UpgradeWizardsService();
}
/**
* @return string
*/
public function getTitle(): string
{
return 'Database Up-to-Date';
}
/**
*/
public function ensure(): bool
{
$adds = $this->upgradeWizardsService->getBlockingDatabaseAdds();
$result = null;
if (count($adds) > 0) {
$this->output->writeln('Performing ' . count($adds) . ' database operations.');
$result = $this->upgradeWizardsService->addMissingTablesAndFields();
}
return $result === null;
}
/**
* @return bool
*/
public function isFulfilled(): bool
{
$adds = $this->upgradeWizardsService->getBlockingDatabaseAdds();
return count($adds) === 0;
}
/**
* @param OutputInterface $output
*/
public function setOutput(OutputInterface $output): void
{
$this->output = $output;
}
}
@@ -0,0 +1,144 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
use TYPO3\CMS\Core\Database\Schema\SqlReader;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Contains the update class for filling the basic repository record of the extension manager
*
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ExtensionManagerTables implements UpgradeWizardInterface, RepeatableInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'extensionManagerTables';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Add the default Extension Manager database tables';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Creates necessary database tables and adds static data for the Extension Manager.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$result = false;
// First check necessary database update
$updateStatements = array_filter($this->getUpdateStatements());
if (count($updateStatements) === 0) {
// Get count of rows in repository database table
$count = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tx_extensionmanager_domain_model_repository')
->count('*', 'tx_extensionmanager_domain_model_repository', []);
if ($count === 0) {
$result = true;
}
} else {
$result = true;
}
return $result;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update.
*
* @return bool Whether it worked (TRUE) or not (FALSE)
*/
public function executeUpdate(): bool
{
$result = true;
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
$createTableStatements = $this->getTableStatements();
// First perform all create, add and change queries
$schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
$schemaMigrationService->install($createTableStatements);
// Perform import of static data
$rawDefinitions = file_get_contents(
ExtensionManagementUtility::extPath('extensionmanager', 'ext_tables_static+adt.sql')
);
$insertStatements = $sqlReader->getInsertStatementArray($rawDefinitions);
$schemaMigrationService->importStaticData($insertStatements);
return $result;
}
/**
* Gets all create, add and change queries from ext_tables.sql
*
* @return array
*/
protected function getUpdateStatements()
{
$updateStatements = [];
$emTableStatements = $this->getTableStatements();
if (count($emTableStatements)) {
$schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
$updateSuggestions = $schemaMigrationService->getUpdateSuggestions($emTableStatements);
$updateStatements = array_merge_recursive(...array_values($updateSuggestions));
}
return $updateStatements;
}
/**
* Get all CREATE TABLE statements from the ext_tables.sql file
*
* @return string[]
*/
protected function getTableStatements(): array
{
$rawDefinitions = file_get_contents(ExtensionManagementUtility::extPath('extensionmanager', 'ext_tables.sql'));
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
return $sqlReader->getCreateTableStatementArray($rawDefinitions);
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Model for extensions installed by upgrade wizards
*
* @internal
*/
class ExtensionModel
{
protected $key = '';
protected $title = '';
protected $versionString = '';
protected $composerName = '';
protected $description = '';
public function __construct(
string $key,
string $title,
string $versionString,
string $composerName,
string $description
) {
$this->key = $key;
$this->title = $title;
$this->versionString = $versionString;
$this->composerName = $composerName;
$this->description = $description;
}
public function getDescription(): string
{
return $this->description;
}
/**
* @return string
*/
public function getKey(): string
{
return $this->key;
}
/**
* @return string
*/
public function getTitle(): string
{
return $this->title;
}
/**
* @return string
*/
public function getVersionString(): string
{
return $this->versionString;
}
/**
* @return string
*/
public function getComposerName(): string
{
return $this->composerName;
}
}
@@ -0,0 +1,136 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\Exception;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Migrate file references that are stored in a wrong way to correct scheme
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class FileReferenceUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'fileReferenceUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate file references that are stored in a wrong way to correct scheme';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'File references were saved in a wrong way and references are not shown correctly in file list module.';
}
/**
* @return bool True if there are records to update
*/
public function updateNecessary(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
return (bool)$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))
)
->execute()
->fetchColumn(0);
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the update
*
* @return bool
*/
public function executeUpdate(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_refindex');
$queryBuilder = $connection->createQueryBuilder();
$statement = $queryBuilder->select('*')
->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))
)
->execute();
while ($record = $statement->fetch()) {
$fileReference = 0;
if (MathUtility::canBeInterpretedAsInteger($record['ref_string'])) {
$fileReference = $record['ref_string'];
} else {
try {
$fileObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($record['ref_string']);
if ($fileObject instanceof File) {
$fileReference = $fileObject->getUid();
}
} catch (Exception $e) {
// Silently catch if there is no file object
}
}
$updateQueryBuilder = $connection->createQueryBuilder();
$updateQueryBuilder->update('sys_refindex')
->where(
$updateQueryBuilder->expr()->eq(
'hash',
$updateQueryBuilder->createNamedParameter($record['hash'], \PDO::PARAM_STR)
)
);
if ($fileReference) {
$updateQueryBuilder->set('ref_table', 'sys_file')
->set('ref_uid', $fileReference)
->set('ref_string', '');
} else {
$updateQueryBuilder->set('deleted', 1);
}
$updateQueryBuilder->execute();
}
return true;
}
}
@@ -0,0 +1,100 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fill translation source field (l10n_source)
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class FillTranslationSourceField implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'fillTranslationSourceField';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Fill translation source field (l10n_source)';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Fill translation source field (l10n_source) for tt_contents which have l18n_parent set.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tt_content');
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$query = $queryBuilder->count('uid')
->from('tt_content')
->where($queryBuilder->expr()->andX(
$queryBuilder->expr()->gt('l18n_parent', $queryBuilder->createNamedParameter(0)),
$queryBuilder->expr()->eq('l10n_source', $queryBuilder->createNamedParameter(0))
));
return (bool)$query->execute()->fetchColumn(0);
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update
*
* @return bool
*/
public function executeUpdate(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tt_content');
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->update('tt_content', 't')
->set('t.l10n_source', 't.l18n_parent', false)
->where($queryBuilder->expr()->andX(
$queryBuilder->expr()->gt('t.l18n_parent', $queryBuilder->createNamedParameter(0)),
$queryBuilder->expr()->eq('t.l10n_source', $queryBuilder->createNamedParameter(0))
))
->execute();
return true;
}
}
@@ -0,0 +1,134 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Installs and downloads EXT:form_legacy if needed
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class FormLegacyExtractionUpdate extends AbstractDownloadExtensionUpdate
{
/**
* @var \TYPO3\CMS\Install\Updates\Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->extension = new ExtensionModel(
'form_legacy',
'Legacy form extension for TYPO3 v7 compatibility',
'8.7.0',
'friendsoftypo3/form-legacy',
'Provides an additional backwards-compatibility layer with legacy functionality for sites that used the form extension in TYPO3 v7.'
);
$this->confirmation = new Confirmation(
'Are you really sure, you want to install EXT:form_legacy?',
'You should install EXT:form_legacy only if you really need it.'
. 'This update wizard checked all content elements and found at least one not deleted element based'
. 'on the old form module. It is advised to manually convert those elements from the old form implementation'
. 'to the new implementation of EXT:form. EXT:form_legacy should be unloaded and removed afterwards.',
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 'formLegacyExtractionUpdate';
}
/**
* Return the speaking name of this wizard
*
* @return string
*/
public function getTitle(): string
{
return 'Install extension "form_legacy"';
}
/**
* Return the description for this wizard
*
* @return string
*/
public function getDescription(): string
{
return 'The extension "form" was rewritten in TYPO3 v8 and follows a new approach.'
. 'This update downloads the old implementation of the form extension as known from TYPO3 v7 from the TER.';
}
/**
* Is an update necessary?
* Is used to determine whether a wizard needs to be run.
*
* @return bool
*/
public function updateNecessary(): bool
{
$updateNeeded = false;
if (!ExtensionManagementUtility::isLoaded('form_legacy')) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$count = $queryBuilder
->count('*')
->from('tt_content')
->where($queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('mailform')))
->execute()
->fetchColumn(0);
if ($count > 0) {
$updateNeeded = true;
}
}
return $updateNeeded;
}
/**
* 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
];
}
}
@@ -0,0 +1,333 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/**
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Doctrine\DBAL\DBALException;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Upgrade wizard which goes through all files referenced in fe_users::image
* and creates sys_file records as well as sys_file_reference records for each hit.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class FrontendUserImageUpdateWizard implements UpgradeWizardInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* Number of records fetched per database query
* Used to prevent memory overflows for huge databases
*/
const RECORDS_PER_QUERY = 1000;
/**
* @var ResourceStorage
*/
protected $storage;
/**
* Table to migrate records from
*
* @var string
*/
protected $table = 'fe_users';
/**
* Table field holding the migration to be
*
* @var string
*/
protected $fieldToMigrate = 'image';
/**
* the source file resides here
*
* @var string
*/
protected $sourcePath = 'uploads/pics/';
/**
* target folder after migration
* Relative to fileadmin
*
* @var string
*/
protected $targetPath = '_migrated/frontend_users/';
/**
* @var Registry
*/
protected $registry;
/**
* @var string
*/
protected $registryNamespace = 'FrontendUserImageUpdateWizard';
/**
* @var array
*/
protected $recordOffset = [];
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'frontendUserImageUpdateWizard';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate all file relations from fe_users.image to sys_file_references';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'This update wizard goes through all files that are referenced in the fe_users.image'
. ' field and adds the files to the FAL File Index. It also moves the files from'
. ' uploads/ to the fileadmin/_migrated/ path.';
}
/**
* Checks if an update is needed
*
* @return bool TRUE if an update is needed, FALSE otherwise
*/
public function updateNecessary(): bool
{
$this->registry = GeneralUtility::makeInstance(Registry::class);
return $this->registry->get($this->registryNamespace, 'recordOffset') === null;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update.
*
* @return bool TRUE on success, FALSE on error
*/
public function executeUpdate(): bool
{
try {
$this->init();
if (!isset($this->recordOffset[$this->table])) {
$this->recordOffset[$this->table] = 0;
}
do {
$limit = $this->recordOffset[$this->table] . ',' . self::RECORDS_PER_QUERY;
$records = $this->getRecordsFromTable($limit);
foreach ($records as $record) {
$this->migrateField($record);
}
$this->registry->set($this->registryNamespace, 'recordOffset', $this->recordOffset);
} while (count($records) === self::RECORDS_PER_QUERY);
$this->registry->remove($this->registryNamespace, 'recordOffset');
} catch (\Exception $e) {
// Silently catch db errors
}
return true;
}
/**
* Initialize the storage repository.
*/
protected function init()
{
$storages = GeneralUtility::makeInstance(StorageRepository::class)->findAll();
$this->storage = $storages[0];
$this->registry = GeneralUtility::makeInstance(Registry::class);
$this->recordOffset = $this->registry->get($this->registryNamespace, 'recordOffset', []);
}
/**
* 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)
*
* @param int $limit Maximum number records to select
* @return array
* @throws \RuntimeException
*/
protected function getRecordsFromTable($limit)
{
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
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')
->setFirstResult($limit)
->execute()
->fetchAll();
} catch (DBALException $e) {
throw new \RuntimeException(
'Database query failed. Error was: ' . $e->getPrevious()->getMessage(),
1476050084
);
}
}
/**
* Migrates a single field.
*
* @param array $row
*/
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::basename($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,
]
);
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();
} else {
$this->recordOffset[$this->table]++;
}
}
}
@@ -0,0 +1,114 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
/**
* Installs and downloads EXT:func
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class FuncExtractionUpdate extends AbstractDownloadExtensionUpdate
{
/**
* @var \TYPO3\CMS\Install\Updates\Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->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 [];
}
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Update sys_language records to use the newly sorting column,
* set default sorting from title
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class LanguageSortingUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'sysLanguageSorting';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Update sorting of sys_language records';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'The sys_language records have unsorted rows. '
. ' This upgrade wizard adds values depending on the language title';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_language');
return (bool)$queryBuilder->count('uid')
->from('sys_language')
->where(
$queryBuilder->expr()->eq('sorting', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
$queryBuilder->expr()->isNotNull('sorting')
)
->execute()
->fetchColumn(0);
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update if the sorting field is 0 or null
*
* @return bool
*/
public function executeUpdate(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_language');
$statement = $queryBuilder->select('uid')
->from('sys_language')
->where(
$queryBuilder->expr()->eq('sorting', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
)
->orderBy('title')
->execute();
$sortCounter = 10;
while ($languageRecord = $statement->fetch()) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_language');
$queryBuilder->update('sys_language')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($languageRecord['uid'], \PDO::PARAM_INT)
)
)
->set('sorting', $sortCounter)
->execute();
$sortCounter += 5;
}
return true;
}
}
@@ -0,0 +1,188 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Doctrine\DBAL\DBALException;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Merge sessions from old fe_session_data table into new structure from fe_sessions
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MigrateFeSessionDataUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'migrateFeSessionDataUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrates existing fe_session_data into fe_sessions';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'With the new Session Framwework the session data is stored in fe_sessions.'
. ' To avoid that data is truncated, ensure the columns of fe_sessions have been updated.'
. ' This wizard migrates the existing data from fe_session_data into fe_sessions.'
. ' Existing entries in fe_sessions having an entry in fe_session_data are updated.'
. ' Entries in fe_session_data not found in fe_sessions are inserted with ses_anonymous = true';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (true) or not (false)
*/
public function updateNecessary(): bool
{
if (!$this->checkIfTableExists('fe_session_data')) {
return false;
}
// Check if there is data to migrate
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('fe_session_data');
$queryBuilder->getRestrictions()->removeAll();
$count = $queryBuilder->count('*')
->from('fe_session_data')
->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 fe_session_data into fe_sessions with respect to ses_anonymous
*
* @return bool
*/
public function executeUpdate(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('fe_sessions');
// Process records that have entries in fe_sessions and fe_session_data
$queryBuilder = $connection->createQueryBuilder();
$statement = $queryBuilder->select('fe_session_data.hash', 'fe_session_data.content')
->from('fe_sessions')
->join(
'fe_sessions',
'fe_session_data',
'fe_session_data',
$queryBuilder->expr()->eq(
'fe_sessions.ses_id',
$queryBuilder->quoteIdentifier('fe_session_data.hash')
)
)
->execute();
$updateQueryBuilder = $connection->createQueryBuilder();
$updateQueryBuilder->update('fe_sessions')
->where(
$updateQueryBuilder->expr()->eq(
'ses_id',
$updateQueryBuilder->createPositionalParameter('', \PDO::PARAM_STR)
)
)
->set('ses_data', $updateQueryBuilder->createPositionalParameter('', \PDO::PARAM_STR), false);
$updateStatement = $connection->prepare($updateQueryBuilder->getSQL());
$connection->beginTransaction();
try {
while ($row = $statement->fetch()) {
$updateStatement->execute([$row['hash'], $row['content']]);
}
$connection->commit();
} catch (DBALException $e) {
$connection->rollBack();
throw $e;
}
// 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',
'fe_sessions',
'fe_sessions',
$queryBuilder->expr()->eq(
'fe_session_data.hash',
$queryBuilder->quoteIdentifier('fe_sessions.ses_id')
)
)
->where($queryBuilder->expr()->isNull('fe_sessions.ses_id'))
->getSQL();
$insertSQL = sprintf(
'INSERT INTO %s(%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
);
try {
$connection->beginTransaction();
$connection->exec($insertSQL);
$connection->commit();
} catch (DBALException $e) {
$connection->rollBack();
throw $e;
}
return true;
}
/**
* Check if given table exists
*
* @param string $table
* @return bool
*/
protected function checkIfTableExists($table): bool
{
$tableExists = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable($table)
->getSchemaManager()
->tablesExist([$table]);
return $tableExists;
}
}
@@ -0,0 +1,141 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Migrate "fluid_styled_content" static template location
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MigrateFscStaticTemplateUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'migrateFscStaticTemplateUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate "fluid_styled_content" static template location';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Static templates have been relocated to EXT:fluid_styled_content/Configuration/TypoScript/';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_template');
$queryBuilder->getRestrictions()->removeAll();
$elementCount = $queryBuilder->count('uid')
->from('sys_template')
->where(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->like(
'constants',
$queryBuilder->createNamedParameter('%EXT:fluid_styled_content/Configuration/TypoScript/Static%', \PDO::PARAM_STR)
),
$queryBuilder->expr()->like(
'config',
$queryBuilder->createNamedParameter('%EXT:fluid_styled_content/Configuration/TypoScript/Static%', \PDO::PARAM_STR)
),
$queryBuilder->expr()->like(
'include_static_file',
$queryBuilder->createNamedParameter('%EXT:fluid_styled_content/Configuration/TypoScript/Static%', \PDO::PARAM_STR)
)
)
)
->execute()->fetchColumn(0);
return (bool)$elementCount;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update
*
* @return bool
*/
public function executeUpdate(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_template');
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder->select('uid', 'include_static_file', 'constants', 'config')
->from('sys_template')
->where(
$queryBuilder->expr()->orX(
$queryBuilder->expr()->like(
'constants',
$queryBuilder->createNamedParameter('%EXT:fluid_styled_content/Configuration/TypoScript/Static%', \PDO::PARAM_STR)
),
$queryBuilder->expr()->like(
'config',
$queryBuilder->createNamedParameter('%EXT:fluid_styled_content/Configuration/TypoScript/Static%', \PDO::PARAM_STR)
),
$queryBuilder->expr()->like(
'include_static_file',
$queryBuilder->createNamedParameter('%EXT:fluid_styled_content/Configuration/TypoScript/Static%', \PDO::PARAM_STR)
)
)
)
->execute();
while ($record = $statement->fetch()) {
$search = 'EXT:fluid_styled_content/Configuration/TypoScript/Static';
$replace = 'EXT:fluid_styled_content/Configuration/TypoScript';
$record['include_static_file'] = str_replace($search, $replace, $record['include_static_file']);
$record['constants'] = str_replace($search, $replace, $record['constants']);
$record['config'] = str_replace($search, $replace, $record['config']);
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->update('sys_template')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
)
)
->set('include_static_file', $record['include_static_file'])
->set('constants', $record['constants'])
->set('config', $record['config']);
$queryBuilder->execute();
}
return true;
}
}
@@ -0,0 +1,140 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Service\UpgradeWizardsService;
/**
* Merge access rights from be_groups concerning pages_language_overlay
* into pages
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MigratePagesLanguageOverlayBeGroupsAccessRights implements UpgradeWizardInterface, ConfirmableInterface
{
public function getIdentifier(): string
{
return 'pagesLanguageOverlayBeGroupsAccessRights';
}
public function getTitle(): string
{
return 'Merge be_groups access rights from pages_language_overlay to pages';
}
public function executeUpdate(): bool
{
$beGroupsQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->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
);
}
}
@@ -0,0 +1,359 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Service\LoadTcaService;
/**
* Merge pages_language_overlay rows into pages table
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MigratePagesLanguageOverlayUpdate implements UpgradeWizardInterface, ChattyInterface
{
/**
* @var OutputInterface
*/
protected $output;
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'pagesLanguageOverlay';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate content from pages_language_overlay to pages';
}
/**
* @return string Longer description of this updater
*/
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 data to the pages '
. 'table by creating new entries and linking them to the l10n parent. This might take a while, '
. 'because max. (amount of pages) x (active languages) new entries need be created.';
}
/**
* Checks whether updates are required.
*
* @return bool Whether an update is required (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
// Check if the database table even exists
if ($this->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();
$this->enableFeatureFlag();
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;
}
/**
* Once the update wizard is run through, the feature to not load any pages_language_overlay data can
* be activated.
*
* Basically writes 'SYS/features/unifiedPageTranslationHandling' to LocalConfiguration.php
*/
protected function enableFeatureFlag()
{
GeneralUtility::makeInstance(ConfigurationManager::class)->enableFeature('unifiedPageTranslationHandling');
}
}
@@ -0,0 +1,156 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Merge URLs divided in pages.urltype and pages.url into pages.url
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MigrateUrlTypesInPagesUpdate implements UpgradeWizardInterface
{
private $databaseTables = ['pages', 'pages_language_overlay'];
private $urltypes = ['', 'http://', 'ftp://', 'mailto:', 'https://'];
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'pagesUrltypeField';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate pages.urltype to pages.url';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'The page property "URL Protocol" for external URLs has been merged into the URL itself.'
. ' The update wizard takes care of properly populating all existing pages and page translations.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (true) or not (false)
*/
public function updateNecessary(): bool
{
if (!$this->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;
}
}
@@ -0,0 +1,266 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Fills pages.slug with a proper value for pages that do not have a slug updater.
* Does not take "deleted" pages into account, but respects workspace records.
*
* This is how it works:
* - Check if a page has pages.alias filled.
* - Check if realurl v1 (tx_realurl_pathcache) or v2 (tx_realurl_pathdata) has a page path, use that instead.
* - If not -> 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
{
protected $table = 'pages';
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 fields with alias are managed first
->orderBy('alias', 'desc')
// 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 = $GLOBALS['TCA'][$this->table]['columns'][$this->fieldName]['config'];
$fieldConfig['generatorOptions']['fields'] = ['tx_realurl_pathsegment,title'];
$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.
if (!empty($record['alias'])) {
$slug = $slugHelper->sanitize('/' . $record['alias']);
}
if (empty($slug)) {
if ($pid === -1) {
$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;
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
class PrerequisiteCollection implements \IteratorAggregate
{
/**
* @var \ArrayObject
*/
protected $prerequisites;
public function __construct()
{
$this->prerequisites = new \ArrayObject();
}
/**
* @param string $prerequisiteClass
*/
public function add(string $prerequisiteClass): void
{
if (
!($this->prerequisites[$prerequisiteClass] ?? false)
&& is_a($prerequisiteClass, PrerequisiteInterface::class, true)
) {
$this->prerequisites[$prerequisiteClass] = GeneralUtility::makeInstance(
$prerequisiteClass
);
}
}
/**
* @return \ArrayObject|\Traversable|PrerequisiteInterface[]
*/
public function getIterator()
{
return $this->prerequisites;
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* UpgradeWizard Prerequisites
*/
interface PrerequisiteInterface
{
/**
* Get speaking name of this prerequisite
*
* @return string
*/
public function getTitle(): string;
/**
* Ensure this prerequisite is fulfilled
*
* Gets called if "isFulfilled" returns false
* and should ensure the prerequisite
*
* Returns true on success, false on error
*
* @see isFulfilled
* @return bool
*/
public function ensure(): bool;
/**
* Is this prerequisite met?
*
* Checks whether this prerequisite is fulfilled. If it is not,
* ensure should be called to fulfill it.
*
* @see ensure
* @return bool
*/
public function isFulfilled(): bool;
}
@@ -0,0 +1,146 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Installs and downloads EXT:rdct if cache_md5params is filled
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class RedirectExtractionUpdate extends AbstractDownloadExtensionUpdate
{
/**
* @var \TYPO3\CMS\Install\Updates\ExtensionModel
*/
protected $extension;
/**
* @var \TYPO3\CMS\Install\Updates\Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->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
];
}
}
@@ -0,0 +1,220 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Installs EXT:redirect if sys_domain.redirectTo is filled, and migrates the values from redirectTo
* to a proper sys_redirect entry.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class RedirectsExtensionUpdate extends AbstractDownloadExtensionUpdate
{
/**
* @var \TYPO3\CMS\Install\Updates\Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->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 superseds 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');
$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 = parse_url($domainName);
$targetDetails = parse_url($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
];
}
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Database\ReferenceIndex;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* ReferenceIndex Prerequisite
*
* Defines that the reference index needs to be up-to-date before an upgrade wizard may be run
*
* @internal
*/
class ReferenceIndexUpdatedPrerequisite implements PrerequisiteInterface, ChattyInterface
{
/**
* @var object|\TYPO3\CMS\Core\Database\ReferenceIndex
*/
private $referenceIndex;
/**
* @var OutputInterface
*/
protected $output;
/**
* ReferenceIndexUpdatedPrerequisite constructor
*/
public function __construct()
{
$this->referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
}
/**
* @inheritdoc
*/
public function getTitle(): string
{
return 'Reference Index Up-to-Date';
}
/**
* Updates the reference index
*
* @return bool
*/
public function ensure(): bool
{
$this->referenceIndex->enableRuntimeCache();
ob_clean();
ob_start();
$result = $this->referenceIndex->updateIndex(false, true);
$output = ob_get_clean();
$this->output->write($output);
return $result[2] === 0;
}
/**
* Checks whether there are reference index updates to be done
*
* @return bool
*/
public function isFulfilled(): bool
{
$this->referenceIndex->enableRuntimeCache();
$result = $this->referenceIndex->updateIndex(true, false);
return $result[2] === 0;
}
/**
* @param OutputInterface $output
*/
public function setOutput(OutputInterface $output): void
{
$this->output = $output;
}
}
@@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Use if wizard may be run multiple times (and should not be disabled after one run)
*
* Semantic/Marker interface only
*/
interface RepeatableInterface
{
}
@@ -0,0 +1,207 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates\RowUpdater;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Migrate values for database records having columns
* using "l10n_mode" set to "mergeIfNotBlank".
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ImageCropUpdater implements RowUpdaterInterface
{
/**
* List of tables with information about to migrate fields.
* Created during hasPotentialUpdateForTable(), used in updateTableRow()
*
* @var array
*/
protected $payload = [];
/**
* Get title
*
* @return string
*/
public function getTitle(): string
{
return 'Migrate values in sys_file_reference crop field';
}
/**
* Return true if a table needs modifications.
*
* @param string $tableName Table name to check
* @return bool True if this table has fields to migrate
*/
public function hasPotentialUpdateForTable(string $tableName): bool
{
$result = false;
$payload = $this->getPayloadForTable($tableName);
if (count($payload) !== 0) {
$this->payload[$tableName] = $payload;
$result = true;
}
return $result;
}
/**
* Update single row if needed
*
* @param string $tableName
* @param array $inputRow Given row data
* @return array Modified row data
*/
public function updateTableRow(string $tableName, array $inputRow): array
{
$tablePayload = $this->payload[$tableName];
foreach ($tablePayload['fields'] as $field) {
if (strpos($inputRow[$field], '{"x":') === 0) {
$cropArray = json_decode($inputRow[$field], true);
if (is_array($cropArray)) {
$file = $this->getFile($inputRow, $tablePayload['fileReferenceField'] ?: 'uid_local');
if (null === $file) {
continue;
}
$cropArea = Area::createFromConfiguration(json_decode($inputRow[$field], true));
$cropVariantCollectionConfig = [
'default' => [
'cropArea' => $cropArea->makeRelativeBasedOnFile($file)->asArray(),
]
];
$inputRow[$field] = json_encode($cropVariantCollectionConfig);
}
}
}
return $inputRow;
}
/**
* Retrieves field names grouped per table name having "l10n_mode" set
* to a relevant value that shall be migrated in database records.
*
* Resulting array is structured like this:
* + fields: [field a, field b, ...]
* + sources
* + source uid: [localization uid, localization uid, ...]
*
* @param string $tableName Table name
* @return array Payload information for this table
* @throws \RuntimeException
*/
protected function getPayloadForTable(string $tableName): array
{
if (!is_array($GLOBALS['TCA'][$tableName])) {
throw new \RuntimeException(
'Globals TCA of given table name must exist',
1485386982
);
}
$tableDefinition = $GLOBALS['TCA'][$tableName];
if (
empty($tableDefinition['columns'])
|| !is_array($tableDefinition['columns'])
) {
return [];
}
$fields = [];
$fileReferenceField = null;
foreach ($tableDefinition['columns'] as $fieldName => $fieldConfiguration) {
if (
!empty($fieldConfiguration['config']['type'])
&& $fieldConfiguration['config']['type'] === 'group'
&& !empty($fieldConfiguration['config']['internal_type'])
&& $fieldConfiguration['config']['internal_type'] === 'db'
&& !empty($fieldConfiguration['config']['allowed'])
&& $fieldConfiguration['config']['allowed'] === 'sys_file'
) {
$fileReferenceField = $fieldName;
}
if (
!empty($fieldConfiguration['config']['type'])
&& $fieldConfiguration['config']['type'] === 'imageManipulation'
) {
$fields[] = $fieldName;
}
}
if (empty($fields)) {
return [];
}
$payload = [];
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
foreach ($fields as $fieldName) {
$queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()->removeAll();
$query = $queryBuilder
->from($tableName)
->count($fieldName)
->where(
$queryBuilder->expr()->like(
$fieldName,
$queryBuilder->createNamedParameter('{"x":%', \PDO::PARAM_STR)
)
);
if ((int)$query->execute()->fetchColumn(0) > 0) {
$payload['fields'][] = $fieldName;
if (isset($fileReferenceField)) {
$payload['fileReferenceField'] = $fileReferenceField;
} else {
$payload['fileReferenceField'] = null;
}
}
}
return $payload;
}
/**
* Get file object
*
* @param array $row
* @param string $fieldName
* @return \TYPO3\CMS\Core\Resource\File|null
*/
private function getFile(array $row, $fieldName)
{
$file = null;
$fileUid = !empty($row[$fieldName]) ? $row[$fieldName] : null;
if (is_array($fileUid) && isset($fileUid[0]['uid'])) {
$fileUid = $fileUid[0]['uid'];
}
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
try {
$file = ResourceFactory::getInstance()->getFileObject((int)$fileUid);
} catch (FileDoesNotExistException $e) {
} catch (\InvalidArgumentException $e) {
}
}
return $file;
}
}
@@ -0,0 +1,314 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates\RowUpdater;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\DataHandling\Localization\State;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Migrate values for database records having columns
* using "l10n_mode" set to "mergeIfNotBlank" or "exclude".
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class L10nModeUpdater implements RowUpdaterInterface
{
/**
* List of tables with information about to migrate fields.
* Created during hasPotentialUpdateForTable(), used in updateTableRow()
*
* @var array
*/
protected $payload = [];
/**
* Get title
*
* @return string
*/
public function getTitle(): string
{
return 'Migrate values in database records having "l10n_mode"'
. ' either set to "exclude" or "mergeIfNotBlank"';
}
/**
* Return true if a table needs modifications.
*
* @param string $tableName Table name to check
* @return bool True if this table has fields to migrate
*/
public function hasPotentialUpdateForTable(string $tableName): bool
{
$this->payload[$tableName] = $this->getL10nModePayloadForTable($tableName);
return !empty($this->payload[$tableName]['localizations']);
}
/**
* Update single row if needed
*
* @param string $tableName
* @param array $inputRow Given row data
* @return array Modified row data
*/
public function updateTableRow(string $tableName, array $inputRow): array
{
$currentId = (int)$inputRow['uid'];
if (empty($this->payload[$tableName]['localizations'][$currentId])) {
return $inputRow;
}
// disable DataHandler hooks for processing this update
if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'])) {
$dataHandlerHooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'];
unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']);
}
if (empty($GLOBALS['LANG'])) {
$GLOBALS['LANG'] = GeneralUtility::makeInstance(LanguageService::class);
}
if (!empty($GLOBALS['BE_USER'])) {
$adminUser = $GLOBALS['BE_USER'];
}
// the admin user is required to defined workspace state when working with DataHandler
$fakeAdminUser = GeneralUtility::makeInstance(BackendUserAuthentication::class);
$fakeAdminUser->user = ['uid' => 0, 'username' => '_migration_', 'admin' => 1];
$fakeAdminUser->workspace = (int)($inputRow['t3ver_wsid'] ?? 0);
$GLOBALS['BE_USER'] = $fakeAdminUser;
$tablePayload = $this->payload[$tableName];
$liveId = $currentId;
if (!empty($inputRow['t3ver_wsid'])
&& !empty($inputRow['t3ver_oid'])
&& !VersionState::cast($inputRow['t3ver_state'])
->equals(VersionState::NEW_PLACEHOLDER_VERSION)) {
$liveId = (int)$inputRow['t3ver_oid'];
}
$dataMap = [];
// define localization states and thus trigger updates later
if (State::isApplicable($tableName)) {
$stateUpdates = [];
foreach ($tablePayload['fieldModes'] as $fieldName => $fieldMode) {
if ($fieldMode !== 'mergeIfNotBlank') {
continue;
}
if (!empty($inputRow[$fieldName])) {
$stateUpdates[$fieldName] = State::STATE_CUSTOM;
} else {
$stateUpdates[$fieldName] = State::STATE_PARENT;
}
}
// fetch the language state upfront, so that calling DataMapProcessor below
// will handle mergeIfNotBlank fields properly
$languageState = State::create($tableName);
$languageState->update($stateUpdates);
$dataMap = [
$tableName => [
$liveId => [
'l10n_state' => $languageState->toArray()
]
]
];
}
// simulate modifying a parent record to trigger dependent updates
if (in_array('exclude', $tablePayload['fieldModes'], true)) {
if ($liveId !== $currentId) {
$record = $this->getRow($tableName, $liveId);
} else {
$record = $inputRow;
}
foreach ($tablePayload['fieldModes'] as $fieldName => $fieldMode) {
if ($fieldMode !== 'exclude') {
continue;
}
$dataMap[$tableName][$liveId][$fieldName] = $record[$fieldName];
}
}
// in case $dataMap is empty, nothing has to be updated
if (!empty($dataMap)) {
// let DataHandler process all updates, $inputRow won't change
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->enableLogging = false;
$dataHandler->start($dataMap, [], $fakeAdminUser);
$dataHandler->process_datamap();
}
if (!empty($dataHandlerHooks)) {
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php'] = $dataHandlerHooks;
}
if (!empty($adminUser)) {
$GLOBALS['BE_USER'] = $adminUser;
}
// the unchanged(!) state as submitted
return $inputRow;
}
/**
* Retrieves field names grouped per table name having "l10n_mode" set
* to a relevant value that shall be migrated in database records.
*
* Resulting array is structured like this:
* + fields: [field a, field b, ...]
* + sources
* + source uid: [localization uid, localization uid, ...]
*
* @param string $tableName Table name
* @return array Payload information for this table
* @throws \RuntimeException
*/
protected function getL10nModePayloadForTable(string $tableName): array
{
if (!isset($GLOBALS['TCA'][$tableName]) || !\is_array($GLOBALS['TCA'][$tableName])) {
throw new \RuntimeException(
'Globals TCA of given table name must exist',
1484176136
);
}
$tableDefinition = $GLOBALS['TCA'][$tableName];
$languageFieldName = ($tableDefinition['ctrl']['languageField'] ?? null);
$parentFieldName = ($tableDefinition['ctrl']['transOrigPointerField'] ?? null);
if (
empty($tableDefinition['columns'])
|| !is_array($tableDefinition['columns'])
|| empty($languageFieldName)
|| empty($parentFieldName)
) {
return [];
}
$fieldModes = [];
foreach ($tableDefinition['columns'] as $fieldName => $fieldConfiguration) {
$l10nMode = ($fieldConfiguration['l10n_mode'] ?? null);
$allowLanguageSynchronization = ($fieldConfiguration['config']['behaviour']['allowLanguageSynchronization'] ?? null);
if ($l10nMode === 'exclude') {
$fieldModes[$fieldName] = $l10nMode;
} elseif ($allowLanguageSynchronization) {
$fieldModes[$fieldName] = 'mergeIfNotBlank';
}
}
if (empty($fieldModes)) {
return [];
}
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$queryBuilder = $connectionPool->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->from($tableName);
$parentFieldName = $tableDefinition['ctrl']['transOrigPointerField'];
$selectFieldNames = ['uid', $parentFieldName];
$predicates = [
$queryBuilder->expr()->gt(
$tableDefinition['ctrl']['languageField'],
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
),
$queryBuilder->expr()->gt(
$parentFieldName,
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
)
];
if (!empty($tableDefinition['ctrl']['versioningWS'])) {
$selectFieldNames = array_merge(
$selectFieldNames,
['t3ver_wsid', 't3ver_oid', 't3ver_state']
);
$predicates[] = $queryBuilder->expr()->orX(
$queryBuilder->expr()->eq(
't3ver_state',
$queryBuilder->createNamedParameter(
VersionState::NEW_PLACEHOLDER_VERSION,
\PDO::PARAM_INT
)
),
$queryBuilder->expr()->eq(
't3ver_state',
$queryBuilder->createNamedParameter(
VersionState::DEFAULT_STATE,
\PDO::PARAM_INT
)
),
$queryBuilder->expr()->eq(
't3ver_state',
$queryBuilder->createNamedParameter(
VersionState::MOVE_POINTER,
\PDO::PARAM_INT
)
)
);
}
$statement = $queryBuilder
->select(...$selectFieldNames)
->andWhere(...$predicates)
->execute();
$payload = [];
foreach ($statement as $row) {
$translationId = $row['uid'];
$parentId = (int)$row[$parentFieldName];
$payload['localizations'][$translationId] = $parentId;
}
if (!empty($payload['localizations'])) {
$payload['fieldModes'] = $fieldModes;
}
return $payload;
}
/**
* @param string $tableName
* @param int $id
* @return array
*/
protected function getRow(string $tableName, int $id)
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($tableName);
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder
->select('*')
->from($tableName)
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
)
)
->execute();
return $statement->fetch();
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates\RowUpdater;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Interface each single row updater must implement.
*/
interface RowUpdaterInterface
{
/**
* Get a description of this single row updater
*
* @return string
*/
public function getTitle(): string;
/**
* Return true if this row updater may have updates for given table rows.
*
* @param string $tableName Given table
* @return bool
*/
public function hasPotentialUpdateForTable(string $tableName): bool;
/**
* Update a single row from a table.
*
* @param string $tableName Given table
* @param array $row Given row
* @return array Potentially modified row
*/
public function updateTableRow(string $tableName, array $row): array;
}
@@ -0,0 +1,211 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates\RowUpdater;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownUrnException;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
/**
* Move '<link ...' syntax to '<a href' in rte fields
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class RteLinkSyntaxUpdater implements RowUpdaterInterface
{
/**
* Table list with field list that may have links them
*
* @var array
*/
protected $tableFieldListToConsider = [];
/**
* @var array Table names that should be ignored.
*/
protected $blackListedTables = [
'sys_log',
'sys_history',
'sys_template',
];
/**
* Regular expressions to match the <link ...>content</link> inside
* @var array
*/
protected $regularExpressions = [
'default' => '#
(?\'tag\'<link\\s++(?\'typolink\'[^>]+)>)
(?\'content\'(?:[^<]++|<(?!/link>))*+)
</link>
#xumsi',
'flex' => '#
(?\'tag\'&lt;link\\s++(?\'typolink\'(?:[^&]++|&(?!gt;))++)&gt;)
(?\'content\'(?:[^&]++|&(?!lt;/link&gt;))*+)
&lt;/link&gt;
#xumsi'
];
/**
* @var LoggerInterface
*/
protected $logger;
/**
* Get title
*
* @return string Title
*/
public function getTitle(): string
{
return 'Scan for old "<link>" syntax in richtext and text fields and update to "<a href>"';
}
/**
* Return true if a table may have RTE fields
*
* @param string $tableName Table name to check
* @return bool True if this table potentially has RTE fields
*/
public function hasPotentialUpdateForTable(string $tableName): bool
{
if (!is_array($GLOBALS['TCA'][$tableName])) {
throw new \RuntimeException(
'Globals TCA of ' . $tableName . ' must be an array',
1484173035
);
}
$result = false;
if (in_array($tableName, $this->blackListedTables, true)) {
return $result;
}
$tcaOfTable = $GLOBALS['TCA'][$tableName];
if (!is_array($tcaOfTable['columns'])) {
return $result;
}
foreach ($tcaOfTable['columns'] as $fieldName => $fieldConfiguration) {
if (isset($fieldConfiguration['config']['type'])
&& in_array($fieldConfiguration['config']['type'], ['input', 'text', 'flex'], true)
) {
$result = true;
if (!is_array($this->tableFieldListToConsider[$tableName])) {
$this->tableFieldListToConsider[$tableName] = [];
}
$this->tableFieldListToConsider[$tableName][] = $fieldName;
}
}
return $result;
}
/**
* Update "<link" tags in RTE fields
*
* @param string $tableName Table name
* @param array $row Given row data
* @return array Modified row data
*/
public function updateTableRow(string $tableName, array $row): array
{
if (!is_array($this->tableFieldListToConsider)) {
throw new \RuntimeException(
'Parent should not call me with a table name I do not consider relevant for update',
1484173650
);
}
$this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
$fieldsToScan = $this->tableFieldListToConsider[$tableName];
foreach ($fieldsToScan as $fieldName) {
$row[$fieldName] = $this->transformLinkTagsIfFound(
$tableName,
$fieldName,
$row,
$GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']['type'] === 'flex'
);
}
return $row;
}
/**
* Finds all <link> tags and calls the typolink codec service and the link service (twice) to get a string
* representation of the href part, and then builds an anchor tag.
*
* @param string $tableName
* @param string $fieldName
* @param array $row
* @param bool $isFlexformField If true the content is htmlspecialchar()'d and must be treated as such
* @return mixed the modified content
*/
protected function transformLinkTagsIfFound(string $tableName, string $fieldName, array $row, bool $isFlexformField)
{
$content = $row[$fieldName];
if (is_string($content)
&& !empty($content)
&& (stripos($content, '<link') !== false || stripos($content, '&lt;link') !== false)
) {
$result = preg_replace_callback(
$this->regularExpressions[$isFlexformField ? 'flex' : 'default'],
function ($matches) use ($isFlexformField) {
$typoLink = $isFlexformField ? htmlspecialchars_decode($matches['typolink']) : $matches['typolink'];
$typoLinkParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($typoLink);
$anchorTagAttributes = [
'target' => $typoLinkParts['target'],
'class' => $typoLinkParts['class'],
'title' => $typoLinkParts['title'],
];
$link = $typoLinkParts['url'];
if (!empty($typoLinkParts['additionalParams'])) {
$link .= (strpos($link, '?') === false ? '?' : '&') . ltrim($typoLinkParts['additionalParams'], '&');
}
try {
$linkService = GeneralUtility::makeInstance(LinkService::class);
// Ensure the old syntax is converted to the new t3:// syntax, if necessary
$linkParts = $linkService->resolve($link);
$anchorTagAttributes['href'] = $linkService->asString($linkParts);
$newLink = '<a ' . GeneralUtility::implodeAttributes($anchorTagAttributes, true) . '>' .
($isFlexformField ? htmlspecialchars_decode($matches['content']) : $matches['content']) .
'</a>';
if ($isFlexformField) {
$newLink = htmlspecialchars($newLink);
}
} catch (UnknownLinkHandlerException $e) {
$newLink = $matches[0];
} catch (UnknownUrnException $e) {
$newLink = $matches[0];
}
return $newLink;
},
$content
);
if ($result !== null) {
$content = $result;
} else {
$this->logger->error('Converting links failed due to PCRE error', [
'table' => $tableName,
'field' => $fieldName,
'uid' => $row['uid'] ?? null,
'errorCode' => preg_last_error()
]);
}
}
return $content;
}
}
@@ -0,0 +1,112 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
/**
* Installs and downloads EXT:rtehtmlarea if needed
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class RteHtmlAreaExtractionUpdate extends AbstractDownloadExtensionUpdate
{
/**
* @var \TYPO3\CMS\Install\Updates\Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->extension = new ExtensionModel(
'rtehtmlarea',
'RTE HTMLArea for TYPO3',
'8.7.0',
'friendsoftypo3/rtehtmlarea',
'The extension provides the well-known RTE used in previous TYPO3 versions, if handling of images or custom legacy configurations are necessary.'
);
$this->confirmation = new Confirmation(
'Are you sure?',
'You should install EXT:rtehtmlarea 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 'rtehtmlareaExtension';
}
/**
* Return the speaking name of this wizard
*
* @return string
*/
public function getTitle(): string
{
return 'Install extension "rtehtmlarea" from TER';
}
/**
* Return the description for this wizard
*
* @return string
*/
public function getDescription(): string
{
return 'The extension "rtehtmlarea" (RTE based on HtmlArea) was extracted into'
. ' the TYPO3 Extension Repository. This update downloads the TYPO3 Extension from the TER.'
. ' Use this if you have special configurations or image handling within Rich Text fields and uninstall the shipped EXT:rte_ckeditor.';
}
/**
* Is an update necessary?
* Is used to determine whether a wizard needs to be run.
*
* @return bool
*/
public function updateNecessary(): bool
{
return !ExtensionManagementUtility::isLoaded('rtehtmlarea');
}
/**
* 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 [];
}
}
@@ -0,0 +1,141 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Migrate the field 'section_frame' for all content elements to 'frame_class'
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class SectionFrameToFrameClassUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'sectionFrameToFrameClassUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate the field "section_frame" for all content elements to "frame_class"';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Section frames were used to control the wrapping of each content element in the frontend '
. 'output, stored as integers in the database. To get rid of a nessesary mapping of those values we '
. 'are now storing strings you can easily adjust that will simply passed to the rendering.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
$tableColumns = $connection->getSchemaManager()->listTableColumns('tt_content');
// Only proceed if section_frame field still exists
if (!isset($tableColumns['section_frame'])) {
return false;
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$elementCount = $queryBuilder->count('uid')
->from('tt_content')
->where(
$queryBuilder->expr()->gt('section_frame', 0)
)
->execute()
->fetchColumn(0);
return (bool)$elementCount;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update
*
* @return bool
*/
public function executeUpdate(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder->select('uid', 'section_frame')
->from('tt_content')
->where(
$queryBuilder->expr()->gt('section_frame', 0)
)
->execute();
while ($record = $statement->fetch()) {
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->update('tt_content')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
)
)
->set('section_frame', 0, false)
->set('frame_class', $this->mapSectionFrame($record['section_frame']));
$queryBuilder->execute();
}
return true;
}
/**
* Map the old to the new values
*
* @param int $sectionFrame The content of the FlexForm
* @return string The equivalent value frame_class
*/
protected function mapSectionFrame($sectionFrame)
{
$mapping = [
0 => 'default',
5 => 'ruler-before',
6 => 'ruler-after',
10 => 'indent',
11 => 'indent-left',
12 => 'indent-right',
66 => 'none'
];
if (array_key_exists($sectionFrame, $mapping)) {
return $mapping[$sectionFrame];
}
return 'custom-' . (int)$sectionFrame;
}
}
@@ -0,0 +1,394 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Merge data stored in sys_log that belongs to sys_history
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class SeparateSysHistoryFromSysLogUpdate implements UpgradeWizardInterface, RepeatableInterface
{
/** @var int Number of records to process in a single query to reduce memory footprint */
private const BATCH_SIZE = 100;
/** @var int Phase that copies data from sys_log to sys_history */
private const MOVE_DATA = 0;
/** @var int Phase that adds history records for inserts and deletes */
private const UPDATE_HISTORY = 1;
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'separateSysHistoryFromLog';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrates existing sys_log entries into sys_history';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'The history of changes of a record is now solely stored within sys_history.'
. ' Previous data within sys_log needs to be migrated into sys_history now.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (true) or not (false)
*/
public function updateNecessary(): bool
{
// sys_log field has been removed, no need to do something.
if (!$this->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 : [];
}
}
@@ -0,0 +1,146 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Split menu types into dedicated content elements
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class SplitMenusUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'splitMenusUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Split menu types into dedicated content elements';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Menus have been split into dedicated content elements to provide '
. 'a better maintainability and more easy to adjustable template with single '
. 'responsibility for the rendering.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
$tableColumns = $connection->getSchemaManager()->listTableColumns('tt_content');
// Only proceed if menu_type field still exists
if (!isset($tableColumns['menu_type'])) {
return false;
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$elementCount = $queryBuilder->count('uid')
->from('tt_content')
->where(
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('menu', \PDO::PARAM_STR))
)
->execute()->fetchColumn(0);
return (bool)$elementCount;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update
*
* @return bool
*/
public function executeUpdate(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder->select('uid', 'header', 'menu_type')
->from('tt_content')
->where(
$queryBuilder->expr()->eq(
'CType',
$queryBuilder->createNamedParameter('menu', \PDO::PARAM_STR)
)
)
->execute();
while ($record = $statement->fetch()) {
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->update('tt_content')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
)
)
->set('CType', $this->mapMenuTypes($record['menu_type']));
$queryBuilder->execute();
}
return true;
}
/**
* Map the old to the new values
*
* @param string $menuType The content of the FlexForm
* @return string The equivalent CType
*/
protected function mapMenuTypes($menuType)
{
$mapping = [
0 => 'menu_pages',
1 => 'menu_subpages',
2 => 'menu_sitemap',
3 => 'menu_section',
4 => 'menu_abstract',
5 => 'menu_recently_updated',
6 => 'menu_related_pages',
7 => 'menu_section_pages',
8 => 'menu_sitemap_pages',
'categorized_pages' => 'menu_categorized_pages',
'categorized_content' => 'menu_categorized_content'
];
if (array_key_exists($menuType, $mapping)) {
return $mapping[$menuType];
}
return 'menu_' . $menuType;
}
}
@@ -0,0 +1,122 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Update backend user setting startModule if set to "help_aboutmodules"
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class StartModuleUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'startModuleUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Update backend user setting "startModule"';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'The backend user setting startModule is changed for the extensions about/aboutmodules.'
. ' Update all backend users that use ext:aboutmodules as startModule.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder->select('uid', 'uc')->from('be_users')->execute();
$needsExecution = false;
while ($backendUser = $statement->fetch()) {
if ($backendUser['uc'] !== null) {
$userConfig = unserialize($backendUser['uc'], ['allowed_classes' => false]);
if ($userConfig['startModule'] === 'help_aboutmodules'
|| $userConfig['startModule'] === 'help_AboutmodulesAboutmodules'
|| $userConfig['startModule'] === 'help_AboutAboutmodules'
) {
$needsExecution = true;
break;
}
}
}
return $needsExecution;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update if backend user's startmodule is
* "help_aboutmodules" or "help_AboutmodulesAboutmodules"
*
* @return bool
*/
public function executeUpdate(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder->select('uid', 'uc')->from('be_users')->execute();
while ($backendUser = $statement->fetch()) {
if ($backendUser['uc'] !== null) {
$userConfig = unserialize($backendUser['uc'], ['allowed_classes' => false]);
if ($userConfig['startModule'] === 'help_aboutmodules'
|| $userConfig['startModule'] === 'help_AboutmodulesAboutmodules'
|| $userConfig['startModule'] === 'help_AboutAboutmodules'
) {
$userConfig['startModule'] = 'help_AboutAbout';
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder->update('be_users')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($backendUser['uid'], \PDO::PARAM_INT)
)
)
->set('uc', $queryBuilder->createNamedParameter(serialize($userConfig), \PDO::PARAM_LOB))
->execute();
}
}
}
return true;
}
}
@@ -0,0 +1,190 @@
<?php
declare(strict_types = 1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\Updates;
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Resource\Security\SvgSanitizer;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class SvgFilesSanitization implements UpgradeWizardInterface, ConfirmableInterface
{
/**
* @var StorageRepository
*/
protected $storageRepository;
/**
* @var Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
$this->confirmation = new Confirmation(
'Continue sanitizing SVG files?',
$this->getDescription(),
false,
'sanitize, backup available',
'cancel',
false
);
}
/**
* 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 'svgFilesSanitization';
}
/**
* Return the speaking name of this wizard
*
* @return string
*/
public function getTitle(): string
{
return 'Sanitize existing SVG files in fileadmin folder';
}
/**
* Return the description for this wizard
*
* @return string
*/
public function getDescription(): string
{
return 'This upgrade wizard will sanitize all SVG files located in local file storages. '
. 'It is very likely that file contents will be changed.' . "\n"
. 'Before continuing, please ensure a proper backup of *.svg and *.svgz files is in place before continuing.';
}
/**
* To avoid timeout issues, no check is performed in advance
*
* @return bool
*/
public function updateNecessary(): bool
{
return true;
}
/**
* Execute the update
*
* Called when a wizard reports that an update is necessary
*
* @return bool
*/
public function executeUpdate(): bool
{
return $this->processSvgFiles();
}
/**
* 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 [];
}
/**
* Return a confirmation message instance
*
* @return Confirmation
*/
public function getConfirmation(): Confirmation
{
return $this->confirmation;
}
/**
* @return ResourceStorage[]
*/
protected function resolveLocalStorages(): array
{
return array_filter(
$this->storageRepository->findByStorageType('Local'),
function (ResourceStorage $storage) {
return $storage->isWritable();
}
);
}
/**
* @param ResourceStorage $storage
* @return File[]
* @throws InsufficientFolderAccessPermissionsException
*/
protected function resolveSvgFiles(ResourceStorage $storage): array
{
$filter = GeneralUtility::makeInstance(FileExtensionFilter::class);
$filter->setAllowedFileExtensions(['svg', 'svgz']);
$files = $storage
->setFileAndFolderNameFilters([
[$filter, 'filterFileList']
])
->getFilesInFolder(
$storage->getRootLevelFolder(),
0,
0,
true,
true
);
$storage->resetFileAndFolderNameFiltersToDefault();
return $files;
}
protected function processSvgFiles(): bool
{
$successful = true;
$sanitizer = GeneralUtility::makeInstance(SvgSanitizer::class);
foreach ($this->resolveLocalStorages() as $storage) {
try {
$svgFiles = $this->resolveSvgFiles($storage);
} catch (InsufficientFolderAccessPermissionsException $exception) {
// @todo Add notice/warning for this upgrade process
$successful = false;
continue;
}
foreach ($svgFiles as $svgFile) {
$oldFileContent = $svgFile->getContents();
$newFileContent = $sanitizer->sanitizeContent($oldFileContent);
if ($oldFileContent !== $newFileContent) {
$svgFile->setContents($newFileContent);
}
}
}
return $successful;
}
}
@@ -0,0 +1,112 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
/**
* Installs and downloads EXT:typo3db_legacy
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class Typo3DbExtractionUpdate extends AbstractDownloadExtensionUpdate
{
/**
* @var \TYPO3\CMS\Install\Updates\Confirmation
*/
protected $confirmation;
public function __construct()
{
$this->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 [];
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
/**
* Interface UpgradeWizardInterface
*/
interface UpgradeWizardInterface
{
/**
* 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 the speaking name of this wizard
*
* @return string
*/
public function getTitle(): string;
/**
* Return the description for this wizard
*
* @return string
*/
public function getDescription(): string;
/**
* Execute the update
*
* Called when a wizard reports that an update is necessary
*
* @return bool
*/
public function executeUpdate(): bool;
/**
* Is an update necessary?
*
* Is used to determine whether a wizard needs to be run.
* Check if data for migration exists.
*
* @return bool
*/
public function updateNecessary(): bool;
/**
* 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;
}
@@ -0,0 +1,112 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Migrate upload content element rendering from layout to uploads_type
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class UploadContentElementUpdate implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'uploadContentElementUpdate';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Migrate upload content element rendering from layout to uploads_type';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Rendering type field has been streamlined with fluid_styled_content.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->getRestrictions()->removeAll();
$elementCount = $queryBuilder->count('uid')
->from('tt_content')
->where(
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('uploads', \PDO::PARAM_STR)),
$queryBuilder->expr()->in('layout', [1, 2])
)
->execute()
->fetchColumn(0);
return (bool)$elementCount;
}
/**
* @return string[] All new fields and tables must exist
*/
public function getPrerequisites(): array
{
return [
DatabaseUpdatedPrerequisite::class
];
}
/**
* Performs the database update
*
* @return bool
*/
public function executeUpdate(): bool
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('tt_content');
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll();
$statement = $queryBuilder->select('uid', 'layout')
->from('tt_content')
->where(
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('uploads', \PDO::PARAM_STR)),
$queryBuilder->expr()->in('layout', [1, 2])
)
->execute();
while ($record = $statement->fetch()) {
$queryBuilder = $connection->createQueryBuilder();
$queryBuilder->update('tt_content')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($record['uid'], \PDO::PARAM_INT)
)
)
->set('layout', 0, false)
->set('uploads_type', $record['layout'])
->execute();
}
return true;
}
}
@@ -0,0 +1,101 @@
<?php
namespace TYPO3\CMS\Install\Updates;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Utility\Exception\MissingArrayPathException;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Move "wizard done" flags to system registry
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class WizardDoneToRegistry implements UpgradeWizardInterface
{
/**
* @return string Unique identifier of this updater
*/
public function getIdentifier(): string
{
return 'wizardDoneToRegistry';
}
/**
* @return string Title of this updater
*/
public function getTitle(): string
{
return 'Move "wizard done" flags from LocalConfiguration.php to system registry';
}
/**
* @return string Longer description of this updater
*/
public function getDescription(): string
{
return 'Moves all "wizard done" flags from LocalConfiguration.php to system registry.';
}
/**
* Checks if an update is needed
*
* @return bool Whether an update is needed (TRUE) or not (FALSE)
*/
public function updateNecessary(): bool
{
$result = false;
try {
$wizardsDone = GeneralUtility::makeInstance(ConfigurationManager::class)
->getLocalConfigurationValueByPath('INSTALL/wizardDone');
if (!empty($wizardsDone)) {
$result = true;
}
} catch (MissingArrayPathException $e) {
// Result stays false with broken path
}
return $result;
}
/**
* @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
{
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
$registry = GeneralUtility::makeInstance(Registry::class);
$wizardsDone = $configurationManager->getLocalConfigurationValueByPath('INSTALL/wizardDone');
$configurationKeysToRemove = [];
foreach ($wizardsDone as $wizardClassName => $value) {
$registry->set('installUpdate', $wizardClassName, $value);
$configurationKeysToRemove[] = 'INSTALL/wizardDone/' . $wizardClassName;
}
$configurationKeysToRemove[] = 'INSTALL/wizardDone';
$configurationManager->removeLocalConfigurationKeysByPath($configurationKeysToRemove);
return true;
}
}