first commit
This commit is contained in:
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\'<link\\s++(?\'typolink\'(?:[^&]++|&(?!gt;))++)>)
|
||||
(?\'content\'(?:[^&]++|&(?!lt;/link>))*+)
|
||||
</link>
|
||||
#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, '<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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user