TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,60 @@
<?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\Backend\History\Event;
use TYPO3\CMS\Backend\History\RecordHistoryRollback;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* This event is fired after a history record rollback finished.
*/
final readonly class AfterHistoryRollbackFinishedEvent
{
public function __construct(
private string $rollbackFields,
private array $diff,
private array $dataHandlerInput,
private RecordHistoryRollback $recordHistoryRollback,
private ?BackendUserAuthentication $backendUserAuthentication = null
) {}
public function getRecordHistoryRollback(): RecordHistoryRollback
{
return $this->recordHistoryRollback;
}
public function getRollbackFields(): string
{
return $this->rollbackFields;
}
public function getDiff(): array
{
return $this->diff;
}
public function getDataHandlerInput(): array
{
return $this->dataHandlerInput;
}
public function getBackendUserAuthentication(): ?BackendUserAuthentication
{
return $this->backendUserAuthentication;
}
}
@@ -0,0 +1,54 @@
<?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\Backend\History\Event;
use TYPO3\CMS\Backend\History\RecordHistoryRollback;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* This event is fired before a history record rollback starts
*/
final readonly class BeforeHistoryRollbackStartEvent
{
public function __construct(
private string $rollbackFields,
private array $diff,
private RecordHistoryRollback $recordHistoryRollback,
private ?BackendUserAuthentication $backendUserAuthentication = null
) {}
public function getRecordHistoryRollback(): RecordHistoryRollback
{
return $this->recordHistoryRollback;
}
public function getRollbackFields(): string
{
return $this->rollbackFields;
}
public function getDiff(): array
{
return $this->diff;
}
public function getBackendUserAuthentication(): ?BackendUserAuthentication
{
return $this->backendUserAuthentication;
}
}
+615
View File
@@ -0,0 +1,615 @@
<?php
/*
* 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\Backend\History;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchema;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Class for fetching the history entries of a record (and if it is a page, its sub elements
* as well)
*
* @internal This class is an implementation detail of the backend record history controller and is not considered part of the public TYPO3 API.
*/
class RecordHistory
{
/**
* Maximum number of sys_history steps to show.
*/
protected int $maxSteps = 20;
/**
* On a pages table - show sub elements as well.
*/
protected bool $showSubElements = true;
/**
* Element reference, syntax [tablename]:[uid]
*/
protected string $element = '';
/**
* sys_history uid which is selected
*/
protected int $lastHistoryEntry = 0;
/**
* Internal cache
*/
protected array $pageAccessCache = [];
/**
* Constructor to define which element to work on - can be overridden with "setLastHistoryEntryNumber"
*
* @param string $element in the form of "tablename:uid"
*/
public function __construct($element = '')
{
$this->element = $this->sanitizeElementValue((string)$element);
}
/**
* If a specific history entry is selected, then the relevant element is resolved for that.
*/
public function setLastHistoryEntryNumber(int $lastHistoryEntry): void
{
$this->lastHistoryEntry = $lastHistoryEntry;
$this->updateCurrentElement();
}
public function getLastHistoryEntryNumber(): int
{
return $this->lastHistoryEntry;
}
/**
* Define the maximum amount of history entries to be shown. Beware of side-effects when using
* "showSubElements" as well.
*/
public function setMaxSteps(int $maxSteps): void
{
$this->maxSteps = $maxSteps;
}
/**
* Defines to show the history of a specific record or its subelements (when it's a page)
* as well.
*/
public function setShowSubElements(bool $showSubElements): void
{
$this->showSubElements = $showSubElements;
}
/**
* Creates change log including sub-elements
*/
public function getChangeLog(): array
{
if (!empty($this->element)) {
[$table, $recordUid] = explode(':', $this->element);
return $this->getHistoryData($table, (int)$recordUid, $this->showSubElements, $this->lastHistoryEntry);
}
return [];
}
/**
* An array (0 = tablename, 1 = uid) or empty array if no element is set
*/
public function getElementInformation(): array
{
return !empty($this->element) ? explode(':', $this->element) : [];
}
/**
* @return string named "tablename:uid"
*/
public function getElementString(): string
{
return $this->element;
}
/**
* Compile a list of record history elements that belong to the same translation root.
*
* @return array{
* page: int,
* elements: array<array{element: string, language: int}>
* }|null
*/
public function getTranslations(string $element): ?array
{
if ($element === '') {
return null;
}
[$table, $recordUid] = explode(':', $element);
$recordUid = (int)$recordUid;
$schema = $this->getTcaSchema($table);
if ($schema === null) {
return null;
}
if (!$schema->isLanguageAware()) {
return null;
}
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
$langField = $languageCapability->getLanguageField()->getName();
$l10nPointer = $languageCapability->getTranslationOriginPointerField()->getName();
$record = BackendUtility::getRecord($table, $recordUid);
$l10nParentUid = (int)($record[$l10nPointer] ?? 0);
$defaultLanguageUid = ($l10nParentUid ?: $recordUid);
if ($defaultLanguageUid === $recordUid) {
$translationRoot = $record;
} else {
$translationRoot = BackendUtility::getRecord($table, $defaultLanguageUid);
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($table);
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
$statement = $queryBuilder->from($table)
->select('*')
->where(
$queryBuilder->expr()->eq($l10nPointer, $queryBuilder->createNamedParameter($defaultLanguageUid))
)
->executeQuery();
$translations = [
'page' => $table === 'pages' ? $defaultLanguageUid : (int)$translationRoot['pid'],
'elements' => [
[
'element' => $table . ':' . $defaultLanguageUid,
'language' => (int)$translationRoot[$langField],
],
],
];
foreach ($statement->fetchAllAssociative() as $row) {
BackendUtility::workspaceOL($table, $row, $this->getBackendUser()->workspace);
if ($row) {
$translations['elements'][] = [
'element' => $table . ':' . $row['uid'],
'language' => (int)$row[$langField],
];
}
}
return $translations;
}
/*******************************
*
* build up history
*
*******************************/
/**
* Creates a diff between the current version of the records and the selected version
*
* @return array Diff for many elements
*/
public function getDiff(array $changeLog): array
{
$insertsDeletes = [];
$newArr = [];
$differences = [];
// traverse changelog array
foreach ($changeLog as $value) {
$field = $value['tablename'] . ':' . $value['recuid'];
// inserts / deletes
if ((int)$value['actiontype'] !== RecordHistoryStore::ACTION_MODIFY) {
if (!isset($insertsDeletes[$field])) {
$insertsDeletes[$field] = 0;
}
($value['action'] ?? '') === 'insert' ? $insertsDeletes[$field]++ : $insertsDeletes[$field]--;
// unset not needed fields
if ($insertsDeletes[$field] === 0) {
unset($insertsDeletes[$field]);
}
} elseif (!isset($newArr[$field])) {
$newArr[$field] = $value['newRecord'];
$differences[$field] = $value['oldRecord'];
} else {
$differences[$field] = array_merge($differences[$field], $value['oldRecord']);
}
}
// remove entries where there were no changes effectively
foreach ($newArr as $record => $value) {
foreach ($value as $key => $innerVal) {
if ($newArr[$record][$key] === $differences[$record][$key]) {
unset($newArr[$record][$key], $differences[$record][$key]);
}
}
if (empty($newArr[$record]) && empty($differences[$record])) {
unset($newArr[$record], $differences[$record]);
}
}
return [
'newData' => $newArr,
'oldData' => $differences,
'insertsDeletes' => $insertsDeletes,
];
}
/**
* Fetches the history data of a record + includes subelements if this is from a page
*
* @param int $lastHistoryEntry the highest entry to be evaluated
*/
protected function getHistoryData(string $table, int $uid, ?bool $includeSubEntries = null, ?int $lastHistoryEntry = null): array
{
$historyDataForRecord = $this->getHistoryDataForRecord($table, $uid, $lastHistoryEntry);
// get history of tables of this page and merge it into changelog
if ($table === 'pages' && $includeSubEntries && $this->hasPageAccess('pages', $uid)) {
foreach ($this->getTcaSchemaFactory()->all()->getNames() as $tablename) {
// check if there are records on the page
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tablename);
$queryBuilder->getRestrictions()->removeAll();
$result = $queryBuilder
->select('uid')
->from($tablename)
->where(
$queryBuilder->expr()->eq(
'pid',
$queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)
)
)
->executeQuery();
while ($row = $result->fetchAssociative()) {
// if there is history data available, merge it into changelog
$newChangeLog = $this->getHistoryDataForRecord($tablename, $row['uid'], $lastHistoryEntry);
foreach ($newChangeLog as $key => $newChangeLogEntry) {
$historyDataForRecord[$key] = $newChangeLogEntry;
}
}
}
}
usort($historyDataForRecord, static function (array $a, array $b): int {
if ($a['tstamp'] < $b['tstamp']) {
return 1;
}
if ($a['tstamp'] > $b['tstamp']) {
return -1;
}
return 0;
});
return $historyDataForRecord;
}
/**
* Gets history and delete/insert data from sys_log and sys_history
*
* @param string $table DB table name
* @param int $uid UID of record
* @param int|null $lastHistoryEntry the highest entry to be fetched
* @return array Array of history data of the record
* @internal
*/
public function getHistoryDataForRecord(string $table, int $uid, ?int $lastHistoryEntry = null): array
{
if (!$this->getTcaSchemaFactory()->has($table) || !$this->hasTableAccess($table) || !$this->hasPageAccess($table, $uid)) {
return [];
}
$uid = $this->resolveElement($table, $uid);
return $this->findEventsForRecord($table, $uid, ($this->maxSteps ?: 0), $lastHistoryEntry);
}
/**
* Get the user uid of the user who deleted the record for the given table
*/
public function getUserIdFromDeleteActionForRecord(string $table, int $uid): int
{
$queryBuilder = $this->getQueryBuilder();
$queryBuilder->select('userid')
->from('sys_history')
->where(
$queryBuilder->expr()->eq(
'tablename',
$queryBuilder->createNamedParameter($table)
),
$queryBuilder->expr()->eq(
'usertype',
$queryBuilder->createNamedParameter('BE')
),
$queryBuilder->expr()->eq(
'recuid',
$queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
'actiontype',
$queryBuilder->createNamedParameter(RecordHistoryStore::ACTION_DELETE, Connection::PARAM_INT)
)
)
->setMaxResults(1);
return (int)$queryBuilder->executeQuery()->fetchOne();
}
/*******************************
*
* Various helper functions
*
*******************************/
/**
* Convert input element reference to workspace version if any.
*
* @param string $table Table of input element
* @param int $uid UID of record
* @return int converted UID of record
*/
protected function resolveElement(string $table, int $uid): int
{
if (!$this->getTcaSchemaFactory()->has($table)) {
return $uid;
}
$workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $uid, 'uid');
if ($workspaceVersion) {
$uid = $workspaceVersion['uid'];
}
return $uid;
}
/**
* Resolve tablename + record uid from sys_history UID
*/
protected function getHistoryEntry(int $lastHistoryEntry): array
{
$queryBuilder = $this->getQueryBuilder();
$record = $queryBuilder
->select('uid', 'tablename', 'recuid')
->from('sys_history')
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($lastHistoryEntry, Connection::PARAM_INT)))
->executeQuery()
->fetchAssociative();
if (empty($record)) {
return [];
}
return $record;
}
/**
* Fetches the history entry for an ADD/creation action for a specific record.
*/
public function getCreationInformationForRecord(string $table, array $record): ?array
{
$queryBuilder = $this->getQueryBuilder();
$result = $queryBuilder
->select('*')
->from('sys_history')
->where(
$queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table)),
$queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($record['uid'], Connection::PARAM_INT)),
$queryBuilder->expr()->eq('actiontype', $queryBuilder->createNamedParameter(RecordHistoryStore::ACTION_ADD, Connection::PARAM_INT))
)
->setMaxResults(1)
->executeQuery()
->fetchAssociative();
return $result ?: null;
}
/**
* Queries the DB and prepares the results
* Resolving a WSOL of the UID and checking permissions is explicitly not part of this method
*/
public function findEventsForRecord(string $table, int $uid, int $limit = 0, ?int $minimumUid = null): array
{
$backendUser = $this->getBackendUser();
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->select('*')
->from('sys_history')
->where(
$queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table)),
$queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT))
);
if ($backendUser->workspace === 0) {
$queryBuilder->andWhere(
$queryBuilder->expr()->eq('workspace', 0)
);
} else {
$queryBuilder->andWhere(
$queryBuilder->expr()->or(
$queryBuilder->expr()->eq('workspace', 0),
$queryBuilder->expr()->eq('workspace', $queryBuilder->createNamedParameter($backendUser->workspace, Connection::PARAM_INT))
)
);
}
if ($limit) {
$queryBuilder->setMaxResults($limit);
}
if ($minimumUid) {
$queryBuilder->andWhere($queryBuilder->expr()->gte('uid', $queryBuilder->createNamedParameter($minimumUid, Connection::PARAM_INT)));
}
return $this->prepareEventDataFromQueryBuilder($queryBuilder);
}
public function findEventsForCorrelation(string $correlationId): array
{
$queryBuilder = $this->getQueryBuilder();
$queryBuilder
->select('*')
->from('sys_history')
->where($queryBuilder->expr()->eq('correlation_id', $queryBuilder->createNamedParameter($correlationId)));
return $this->prepareEventDataFromQueryBuilder($queryBuilder);
}
protected function prepareEventDataFromQueryBuilder(QueryBuilder $queryBuilder): array
{
$events = [];
$result = $queryBuilder->orderBy('tstamp', 'DESC')->executeQuery();
while ($row = $result->fetchAssociative()) {
$identifier = (int)$row['uid'];
$actionType = (int)$row['actiontype'];
if ($actionType === RecordHistoryStore::ACTION_ADD || $actionType === RecordHistoryStore::ACTION_UNDELETE) {
$row['action'] = 'insert';
}
if ($actionType === RecordHistoryStore::ACTION_DELETE) {
$row['action'] = 'delete';
}
if ($actionType === RecordHistoryStore::ACTION_PUBLISH) {
$row['action'] = 'publish';
}
if ($row['history_data'] === null) {
$events[$identifier] = $row;
continue;
}
if (str_starts_with($row['history_data'], 'a')) {
// legacy code
$row['history_data'] = unserialize($row['history_data'], ['allowed_classes' => false]);
} else {
$row['history_data'] = json_decode($row['history_data'], true);
}
if (isset($row['history_data']['newRecord'])) {
$row['newRecord'] = $row['history_data']['newRecord'];
}
if (isset($row['history_data']['oldRecord'])) {
$row['oldRecord'] = $row['history_data']['oldRecord'];
}
$events[$identifier] = $row;
}
krsort($events);
return $events;
}
/**
* Determines whether user has access to a page.
*/
protected function hasPageAccess(string $table, int $uid): bool
{
$pageRecord = null;
if ($table === 'pages') {
$pageId = $uid;
} else {
$record = BackendUtility::getRecord($table, $uid, '*', '', false);
$pageId = ($record['pid'] ?? 0);
}
$schema = $this->getTcaSchema($table);
if ($pageId === 0 && ($schema?->getCapability(TcaSchemaCapability::RestrictionRootLevel)->shallIgnoreRootLevelRestriction() ?? false)) {
return true;
}
if (!isset($this->pageAccessCache[$pageId])) {
$isDeletedPage = false;
$pageSchema = $this->getTcaSchemaFactory()->get('pages');
if ($pageSchema->hasCapability(TcaSchemaCapability::SoftDelete)) {
$deletedField = $pageSchema->getCapability(TcaSchemaCapability::SoftDelete)->getFieldName();
$pageRecord = BackendUtility::getRecord('pages', $pageId, '*', '', false);
$isDeletedPage = (bool)($pageRecord[$deletedField] ?? false);
}
if ($isDeletedPage) {
// The page is deleted, so we fake its uid to be the one of the parent page.
// By doing so, the following API will use this id to traverse the rootline
// and check whether it is in the users' web mounts.
// We check however if the user has (or better had) access to the deleted page itself.
// Since the only way we got here is by requesting the history of the parent page
// we can be sure this parent page actually exists.
$pageRecord['uid'] = $pageRecord['pid'];
$this->pageAccessCache[$pageId] = $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::PAGE_SHOW);
} else {
$this->pageAccessCache[$pageId] = BackendUtility::readPageAccess(
$pageId,
$this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
);
}
}
return $this->pageAccessCache[$pageId] !== false;
}
/**
* Sanitizes the values for the expected disposal.
* Invalid values will be converted to an empty string.
*
* @param string $value the value of the element value
*/
protected function sanitizeElementValue(string $value): string
{
if ($value !== '' && !preg_match('#^[a-z\d_.]+:[\d]+$#i', $value)) {
return '';
}
return $value;
}
/**
* Determines whether user has access to a table.
*/
protected function hasTableAccess(string $table): bool
{
return $this->getBackendUser()->check('tables_select', $table);
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
protected function getQueryBuilder(): QueryBuilder
{
return GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_history');
}
protected function getTcaSchemaFactory(): TcaSchemaFactory
{
return GeneralUtility::makeInstance(TcaSchemaFactory::class);
}
protected function getTcaSchema(string $table): ?TcaSchema
{
$schemaFactory = $this->getTcaSchemaFactory();
if ($schemaFactory->has($table)) {
return $schemaFactory->get($table);
}
return null;
}
protected function updateCurrentElement(): void
{
if ($this->lastHistoryEntry) {
$elementData = $this->getHistoryEntry($this->lastHistoryEntry);
if (!empty($elementData) && empty($this->element)) {
$this->element = $elementData['tablename'] . ':' . $elementData['recuid'];
}
}
}
}
+112
View File
@@ -0,0 +1,112 @@
<?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\Backend\History;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\History\Event\AfterHistoryRollbackFinishedEvent;
use TYPO3\CMS\Backend\History\Event\BeforeHistoryRollbackStartEvent;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
#[Autoconfigure(public: true)]
readonly class RecordHistoryRollback
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
) {}
/**
* Perform rollback via DataHandler
*/
public function performRollback(string $rollbackFields, array $diff, ?BackendUserAuthentication $backendUserAuthentication = null): void
{
$this->eventDispatcher->dispatch(new BeforeHistoryRollbackStartEvent($rollbackFields, $diff, $this, $backendUserAuthentication));
$rollbackData = explode(':', $rollbackFields);
$rollbackDataCount = count($rollbackData);
// PROCESS INSERTS AND DELETES
// rewrite inserts and deletes
$commandMapArray = [];
$data = [];
if ($diff['insertsDeletes']) {
if ($rollbackDataCount === 1) {
// all tables
$data = $diff['insertsDeletes'];
} elseif ($rollbackDataCount === 2 && $diff['insertsDeletes'][$rollbackFields]) {
// one record
$data[$rollbackFields] = $diff['insertsDeletes'][$rollbackFields];
}
if (!empty($data)) {
foreach ($data as $key => $action) {
$elParts = explode(':', $key);
if ((int)$action === 1) {
// inserted records should be deleted
$commandMapArray[$elParts[0]][$elParts[1]]['delete'] = 1;
// When the record is deleted, the contents of the record do not need to be updated
unset(
$diff['oldData'][$key],
$diff['newData'][$key]
);
} elseif ((int)$action === -1) {
// deleted records should be inserted again
$commandMapArray[$elParts[0]][$elParts[1]]['undelete'] = 1;
}
}
}
}
// Writes the data:
if ($commandMapArray) {
$tce = GeneralUtility::makeInstance(DataHandler::class);
$tce->dontProcessTransformations = true;
$tce->start([], $commandMapArray, $backendUserAuthentication);
$tce->process_cmdmap();
unset($tce);
}
if ($diff['oldData'] ?? false) {
// PROCESS CHANGES
// create an array for process_datamap
$diffModified = [];
foreach ($diff['oldData'] as $key => $value) {
$splitKey = explode(':', $key);
$diffModified[$splitKey[0]][$splitKey[1]] = $value;
}
if ($rollbackDataCount === 1) {
// all tables
$data = $diffModified;
} elseif ($rollbackDataCount === 2) {
// one record
$data[$rollbackData[0]][$rollbackData[1]] = $diffModified[$rollbackData[0]][$rollbackData[1]];
} elseif ($rollbackDataCount === 3) {
// one field in one record
$data[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]] = $diffModified[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]];
}
// Writes the data:
$tce = GeneralUtility::makeInstance(DataHandler::class);
$tce->dontProcessTransformations = true;
$tce->start($data, [], $backendUserAuthentication);
$tce->process_datamap();
unset($tce);
}
if (isset($data['pages']) || isset($commandMapArray['pages'])) {
BackendUtility::setUpdateSignal('updatePageTree');
}
$this->eventDispatcher->dispatch(new AfterHistoryRollbackFinishedEvent($rollbackFields, $diff, $data, $this, $backendUserAuthentication));
}
}