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,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;
}
}