TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user