From d047b314162ad1683f116df762384ada8b9dd5f3 Mon Sep 17 00:00:00 2001 From: Benni Mack <benni@typo3.org> Date: Tue, 13 Jun 2017 07:17:18 +0200 Subject: [PATCH] [!!!][BUGFIX] Separate sys_history from sys_log db entries Before, the history module fetched info about "modified records" from sys_history+the authoritive user from a coupled sys_log entry. Info about "insert" and "delete" was fetched from sys_log solely. However, when using a scheduled cleanup task to truncate sys_log then all history information is useless (see bug report). The patch introduces a new RecordHistoryStore as an abstraction for adding history entries (currently done solely within DataHandler). It adds some additional, necessary SQL fields to sys_history to store all information in there and creates an update wizard to migrate all coupled sys_history/sys_log entries to a new sys_history entry itself. Additionally, the whole existing "RecordHistory" class is now only necessary for fetching the so-called ChangeLog, for a page or a specific record, and to do rollbacks, preparing the history records so they can be worked on. The whole logic for fetching the GET/POST parameters is moved into the "ElementHistoryController", everything that is only possible via Fluid is moved from the RecordHistory object and the ElementHistoryController into the view. Referencing from sys_log (Log module) into sys_history is now done the other way around, storing information about the corresponding history entry inside sys_log. As a side-effect, sys_log should load faster. Abstraction basis: - sys_history is the only source of truth about the history of a record - sys_log contains a reference to an history entry now (inside sys_log.log_data) to link from the backend log module - RecordHistoryStore exists for tracking changes to records - RecordHistory is for retrieving, compiling the history/changelog and rollbacks - ElementHistoryController is doing PSR-7 style request/response handling and preparing data for the view - Fluid is handling more view functionality now, removing the need for doing <f:format.raw> everywhere in the templates. Sidenotes: * Data within sys_history is now stored as JSON, not serialized anymore * Adding/deleting was previously stored in sys_log only, is now within sys_history * Moving records is now tracked (but not evaluated yet) * Highlight/Snapshot functionality within the Backend Module was removed This functionality is built so it can also be used within Extbase persistence and in FE in general in a future iteration. Resolves: #55298 Resolves: #71950 Releases: master Change-Id: I354317609099bac10c264b9932e331fa908c98be Reviewed-on: https://review.typo3.org/53195 Reviewed-by: Andreas Fernandez <typo3@scripting-base.de> Tested-by: Andreas Fernandez <typo3@scripting-base.de> Tested-by: TYPO3com <no-reply@typo3.com> Reviewed-by: Joerg Kummer <typo3@enobe.de> Tested-by: Joerg Kummer <typo3@enobe.de> Reviewed-by: Benni Mack <benni@typo3.org> Tested-by: Benni Mack <benni@typo3.org> --- .../ElementHistoryController.php | 391 +++++++- .../backend/Classes/History/RecordHistory.php | 904 +++++------------- .../Private/Partials/RecordHistory/Diff.html | 9 +- .../Partials/RecordHistory/History.html | 42 +- .../Partials/RecordHistory/MultipleDiff.html | 6 +- .../Partials/RecordHistory/Settings.html | 7 +- .../Private/Templates/RecordHistory/Main.html | 12 +- .../Classes/Domain/Model/HistoryEntry.php | 49 - .../Repository/HistoryEntryRepository.php | 32 - .../ViewHelpers/FormatDetailsViewHelper.php | 4 +- .../ViewHelpers/HistoryEntryViewHelper.php | 102 -- .../belog/Configuration/TypoScript/setup.txt | 5 - .../Private/Partials/Content/LogEntries.html | 6 +- .../Repository/HistoryEntryRepositoryTest.php | 47 - .../core/Classes/DataHandling/DataHandler.php | 58 +- .../Classes/History/RecordHistoryStore.php | 183 ++++ .../core/Configuration/TCA/sys_history.php | 20 +- ...ng-55298-DecoupledHistoryFunctionality.rst | 99 ++ typo3/sysext/core/ext_tables.sql | 15 +- .../SeparateSysHistoryFromSysLogUpdate.php | 175 ++++ .../ExtensionScanner/Php/ClassNameMatcher.php | 15 + .../Php/MethodArgumentDroppedMatcher.php | 6 + .../Php/MethodCallMatcher.php | 93 +- .../Php/PropertyProtectedMatcher.php | 15 + .../Php/PropertyPublicMatcher.php | 35 + typo3/sysext/install/ext_localconf.php | 2 + .../Classes/Service/HistoryService.php | 22 +- 27 files changed, 1299 insertions(+), 1055 deletions(-) delete mode 100644 typo3/sysext/belog/Classes/Domain/Model/HistoryEntry.php delete mode 100644 typo3/sysext/belog/Classes/Domain/Repository/HistoryEntryRepository.php delete mode 100644 typo3/sysext/belog/Classes/ViewHelpers/HistoryEntryViewHelper.php delete mode 100644 typo3/sysext/belog/Tests/Unit/Domain/Repository/HistoryEntryRepositoryTest.php create mode 100644 typo3/sysext/core/Classes/History/RecordHistoryStore.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Breaking-55298-DecoupledHistoryFunctionality.rst create mode 100644 typo3/sysext/install/Classes/Updates/SeparateSysHistoryFromSysLogUpdate.php diff --git a/typo3/sysext/backend/Classes/Controller/ContentElement/ElementHistoryController.php b/typo3/sysext/backend/Classes/Controller/ContentElement/ElementHistoryController.php index 45ecccacee06..e45a75080b8a 100644 --- a/typo3/sysext/backend/Classes/Controller/ContentElement/ElementHistoryController.php +++ b/typo3/sysext/backend/Classes/Controller/ContentElement/ElementHistoryController.php @@ -19,33 +19,45 @@ use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Backend\History\RecordHistory; use TYPO3\CMS\Backend\Module\AbstractModule; use TYPO3\CMS\Backend\Template\Components\ButtonBar; -use TYPO3\CMS\Backend\Template\DocumentTemplate; use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\History\RecordHistoryStore; use TYPO3\CMS\Core\Imaging\Icon; +use TYPO3\CMS\Core\Utility\DiffUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Fluid\View\StandaloneView; /** - * Script Class for showing the history module of TYPO3s backend + * Controller for showing the history module of TYPO3s backend * @see \TYPO3\CMS\Backend\History\RecordHistory */ class ElementHistoryController extends AbstractModule { /** - * @var string + * @var ServerRequestInterface */ - public $content; + protected $request; /** - * Document template object + * @var StandaloneView + */ + protected $view; + + /** + * @var RecordHistory + */ + protected $historyObject; + + /** + * Display diff or not (0-no diff, 1-inline) * - * @var DocumentTemplate + * @var int */ - public $doc; + protected $showDiff = 1; /** * @var array */ - protected $pageInfo; + protected $recordCache = []; /** * Constructor @@ -53,10 +65,7 @@ class ElementHistoryController extends AbstractModule public function __construct() { parent::__construct(); - $this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf'); - $GLOBALS['SOBE'] = $this; - - $this->init(); + $this->view = $this->initializeView(); } /** @@ -69,43 +78,68 @@ class ElementHistoryController extends AbstractModule */ public function mainAction(ServerRequestInterface $request, ResponseInterface $response) { - $this->main(); - - $response->getBody()->write($this->moduleTemplate->renderContent()); - return $response; - } + $this->request = $request; + $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation([]); - /** - * Initialize the module output - */ - protected function init() - { - // Create internal template object - // This is ugly, we need to remove the dependency-wiring via GLOBALS['SOBE'] - // In this case, RecordHistory.php depends on GLOBALS[SOBE] being set in here - $this->doc = GeneralUtility::makeInstance(DocumentTemplate::class); - } + $lastHistoryEntry = (int)($request->getParsedBody()['historyEntry'] ?: $request->getQueryParams()['historyEntry']); + $rollbackFields = $request->getParsedBody()['rollbackFields'] ?: $request->getQueryParams()['rollbackFields']; + $element = $request->getParsedBody()['element'] ?: $request->getQueryParams()['element']; + $displaySettings = $this->prepareDisplaySettings(); + $this->view->assign('currentSelection', $displaySettings); - /** - * Generate module output - */ - public function main() - { - $this->content = '<h1>' . $this->getLanguageService()->getLL('title') . '</h1>'; - $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation([]); + $this->showDiff = (int)$displaySettings['showDiff']; // Start history object - $historyObj = GeneralUtility::makeInstance(RecordHistory::class); + $this->historyObject = GeneralUtility::makeInstance(RecordHistory::class, $element, $rollbackFields); + $this->historyObject->setShowSubElements((int)$displaySettings['showSubElements']); + $this->historyObject->setLastHistoryEntry($lastHistoryEntry); + if ($displaySettings['maxSteps']) { + $this->historyObject->setMaxSteps((int)$displaySettings['maxSteps']); + } + + // Do the actual logic now (rollback, show a diff for certain changes, + // or show the full history of a page or a specific record) + $this->historyObject->createChangeLog(); + if (!empty($this->historyObject->changeLog)) { + if ($this->historyObject->shouldPerformRollback()) { + $this->historyObject->performRollback(); + } elseif ($lastHistoryEntry) { + $completeDiff = $this->historyObject->createMultipleDiff(); + $this->displayMultipleDiff($completeDiff); + $this->view->assign('showDifferences', true); + $this->view->assign('fullViewUrl', $this->buildUrl(['historyEntry' => ''])); + } + if ($this->historyObject->getElementData()) { + $this->displayHistory($this->historyObject->changeLog); + } + } + + $elementData = $this->historyObject->getElementData(); + if ($elementData) { + $this->setPagePath($elementData[0], $elementData[1]); + // Get link to page history if the element history is shown + if ($elementData[0] !== 'pages') { + $this->view->assign('singleElement', true); + $parentPage = BackendUtility::getRecord($elementData[0], $elementData[1], '*', '', false); + if ($parentPage['pid'] > 0 && BackendUtility::readPageAccess($parentPage['pid'], $this->getBackendUser()->getPagePermsClause(1))) { + $this->view->assign('fullHistoryUrl', $this->buildUrl([ + 'element' => 'pages:' . $parentPage['pid'], + 'historyEntry' => '', + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') + ])); + } + } + } - $elementData = GeneralUtility::trimExplode(':', $historyObj->element); - $this->setPagePath($elementData[0], $elementData[1]); + $this->view->assign('TYPO3_REQUEST_URI', GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')); - // Get content: - $this->content .= $historyObj->main(); // Setting up the buttons and markers for docheader $this->getButtons(); // Build the <body> for the module - $this->moduleTemplate->setContent($this->content); + $this->moduleTemplate->setContent($this->view->render()); + + $response->getBody()->write($this->moduleTemplate->renderContent()); + return $response; } /** @@ -133,8 +167,6 @@ class ElementHistoryController extends AbstractModule /** * Create the panel of buttons for submitting the form or otherwise perform operations. - * - * @return array All available buttons as an assoc. array */ protected function getButtons() { @@ -156,6 +188,281 @@ class ElementHistoryController extends AbstractModule } } + /** + * Displays settings evaluation + */ + protected function prepareDisplaySettings() + { + // Get current selection from UC, merge data, write it back to UC + $currentSelection = is_array($this->getBackendUser()->uc['moduleData']['history']) + ? $this->getBackendUser()->uc['moduleData']['history'] + : ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1]; + $currentSelectionOverride = $this->request->getParsedBody()['settings'] ? $this->request->getParsedBody()['settings'] : $this->request->getQueryParams()['settings']; + if (is_array($currentSelectionOverride) && !empty($currentSelectionOverride)) { + $currentSelection = array_merge($currentSelection, $currentSelectionOverride); + $this->getBackendUser()->uc['moduleData']['history'] = $currentSelection; + $this->getBackendUser()->writeUC($this->getBackendUser()->uc); + } + // Display selector for number of history entries + $selector['maxSteps'] = [ + 10 => [ + 'value' => 10 + ], + 20 => [ + 'value' => 20 + ], + 50 => [ + 'value' => 50 + ], + 100 => [ + 'value' => 100 + ], + 999 => [ + 'value' => 'maxSteps_all' + ] + ]; + $selector['showDiff'] = [ + 0 => [ + 'value' => 'showDiff_no' + ], + 1 => [ + 'value' => 'showDiff_inline' + ] + ]; + $selector['showSubElements'] = [ + 0 => [ + 'value' => 'no' + ], + 1 => [ + 'value' => 'yes' + ] + ]; + + $scriptUrl = GeneralUtility::linkThisScript(); + + foreach ($selector as $key => $values) { + foreach ($values as $singleKey => $singleVal) { + $selector[$key][$singleKey]['scriptUrl'] = htmlspecialchars(GeneralUtility::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey)); + } + } + $this->view->assign('settings', $selector); + return $currentSelection; + } + + /** + * Displays a diff over multiple fields including rollback links + * + * @param array $diff Difference array + */ + protected function displayMultipleDiff(array $diff) + { + // Get all array keys needed + $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData'])); + $arrayKeys = array_unique($arrayKeys); + if (!empty($arrayKeys)) { + $lines = []; + foreach ($arrayKeys as $key) { + $singleLine = []; + $elParts = explode(':', $key); + // Turn around diff because it should be a "rollback preview" + if ((int)$diff['insertsDeletes'][$key] === 1) { + // insert + $singleLine['insertDelete'] = 'delete'; + } elseif ((int)$diff['insertsDeletes'][$key] === -1) { + $singleLine['insertDelete'] = 'insert'; + } + // Build up temporary diff array + // turn around diff because it should be a "rollback preview" + if ($diff['newData'][$key]) { + $tmpArr = [ + 'newRecord' => $diff['oldData'][$key], + 'oldRecord' => $diff['newData'][$key] + ]; + $singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], $elParts[1]); + } + $elParts = explode(':', $key); + $singleLine['revertRecordUrl'] = $this->buildUrl(['rollbackFields' => $key]); + $singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]); + $lines[] = $singleLine; + } + $this->view->assign('revertAllUrl', $this->buildUrl(['rollbackFields' => 'ALL'])); + $this->view->assign('multipleDiff', $lines); + } + } + + /** + * Shows the full change log + * + * @param array $historyEntries + */ + protected function displayHistory(array $historyEntries) + { + if (empty($historyEntries)) { + return; + } + $languageService = $this->getLanguageService(); + $lines = []; + $beUserArray = BackendUtility::getUserNames(); + + // Traverse changeLog array: + foreach ($historyEntries as $entry) { + // Build up single line + $singleLine = []; + + // Get user names + $singleLine['backendUserUid'] = $entry['userid']; + $singleLine['backendUserName'] = $entry['userid'] ? $beUserArray[$entry['userid']]['username'] : ''; + // Executed by switch user + if (!empty($entry['originaluserid'])) { + $singleLine['originalBackendUserName'] = $beUserArray[$entry['originaluserid']]['username']; + } + + // Diff link + $singleLine['diffUrl'] = $this->buildUrl(['historyEntry' => $entry['uid']]); + // Add time + $singleLine['time'] = BackendUtility::datetime($entry['tstamp']); + // Add age + $singleLine['age'] = BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')); + + $singleLine['title'] = $this->generateTitle($entry['tablename'], $entry['recuid']); + $singleLine['elementUrl'] = $this->buildUrl(['element' => $entry['tablename'] . ':' . $entry['recuid']]); + if ((int)$entry['actiontype'] === RecordHistoryStore::ACTION_MODIFY) { + // show changes + if (!$this->showDiff) { + // Display field names instead of full diff + // Re-write field names with labels + $tmpFieldList = array_keys($entry['newRecord']); + foreach ($tmpFieldList as $key => $value) { + $tmp = str_replace(':', '', $languageService->sL(BackendUtility::getItemLabel($entry['tablename'], $value))); + if ($tmp) { + $tmpFieldList[$key] = $tmp; + } else { + // remove fields if no label available + unset($tmpFieldList[$key]); + } + } + $singleLine['fieldNames'] = implode(',', $tmpFieldList); + } else { + // Display diff + $singleLine['differences'] = $this->renderDiff($entry, $entry['tablename']); + } + } + // put line together + $lines[] = $singleLine; + } + $this->view->assign('history', $lines); + } + + /** + * Renders HTML table-rows with the comparison information of an sys_history entry record + * + * @param array $entry sys_history entry record. + * @param string $table The table name + * @param int $rollbackUid If set to UID of record, display rollback links + * @return array array of records + */ + protected function renderDiff($entry, $table, $rollbackUid = 0): array + { + $lines = []; + if (is_array($entry['newRecord'])) { + /* @var DiffUtility $diffUtility */ + $diffUtility = GeneralUtility::makeInstance(DiffUtility::class); + $diffUtility->stripTags = false; + $fieldsToDisplay = array_keys($entry['newRecord']); + $languageService = $this->getLanguageService(); + foreach ($fieldsToDisplay as $fN) { + if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') { + // Create diff-result: + $diffres = $diffUtility->makeDiffDisplay( + BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true), + BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true) + ); + $rollbackUrl = ''; + if ($rollbackUid) { + $rollbackUrl = $this->buildUrl(['rollbackFields' => ($table . ':' . $rollbackUid . ':' . $fN)]); + } + $lines[] = [ + 'title' => $languageService->sL(BackendUtility::getItemLabel($table, $fN)), + 'rollbackUrl' => $rollbackUrl, + 'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres)) + ]; + } + } + } + return $lines; + } + + /** + * Generates the URL for a link to the current page + * + * @param array $overrideParameters + * @return string + */ + protected function buildUrl($overrideParameters = []): string + { + $params = []; + // Setting default values based on GET parameters: + if ($this->historyObject->getElementData()) { + $params['element'] = $this->historyObject->getElementString(); + } + $params['historyEntry'] = $this->historyObject->lastHistoryEntry; + // Merging overriding values: + $params = array_merge($params, $overrideParameters); + // Make the link: + return BackendUtility::getModuleUrl('record_history', $params); + } + + /** + * Generates the title and puts the record title behind + * + * @param string $table + * @param string $uid + * @return string + */ + protected function generateTitle($table, $uid): string + { + $title = $table . ':' . $uid; + if (!empty($GLOBALS['TCA'][$table]['ctrl']['label'])) { + $record = $this->getRecord($table, $uid); + $title .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')'; + } + return $title; + } + + /** + * Gets a database record (cached). + * + * @param string $table + * @param int $uid + * @return array|NULL + */ + protected function getRecord($table, $uid) + { + if (!isset($this->recordCache[$table][$uid])) { + $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false); + } + return $this->recordCache[$table][$uid]; + } + + /** + * Returns a new standalone view, shorthand function + * + * @return StandaloneView + */ + protected function initializeView() + { + /** @var StandaloneView $view */ + $view = GeneralUtility::makeInstance(StandaloneView::class); + $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]); + $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]); + $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]); + + $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html')); + + $view->getRequest()->setControllerExtensionName('Backend'); + return $view; + } + /** * Returns LanguageService * diff --git a/typo3/sysext/backend/Classes/History/RecordHistory.php b/typo3/sysext/backend/Classes/History/RecordHistory.php index 619ae5b6c25a..d1d50f313e48 100644 --- a/typo3/sysext/backend/Classes/History/RecordHistory.php +++ b/typo3/sysext/backend/Classes/History/RecordHistory.php @@ -16,15 +16,14 @@ namespace TYPO3\CMS\Backend\History; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Database\Query\QueryBuilder; use TYPO3\CMS\Core\DataHandling\DataHandler; -use TYPO3\CMS\Core\Imaging\Icon; -use TYPO3\CMS\Core\Imaging\IconFactory; -use TYPO3\CMS\Core\Utility\DiffUtility; +use TYPO3\CMS\Core\History\RecordHistoryStore; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; /** - * Class for the record history display module show_rechis + * Class for fetching the history entries of a record (and if it is a page, its subelements + * as well) */ class RecordHistory { @@ -33,47 +32,28 @@ class RecordHistory * * @var int */ - public $maxSteps = 20; - - /** - * Display diff or not (0-no diff, 1-inline) - * - * @var int - */ - public $showDiff = 1; + protected $maxSteps = 20; /** * On a pages table - show sub elements as well. * * @var int */ - public $showSubElements = 1; - - /** - * Show inserts and deletes as well - * - * @var int - */ - public $showInsertDelete = 1; + protected $showSubElements = 1; /** * Element reference, syntax [tablename]:[uid] * * @var string */ - public $element; + protected $element; /** - * syslog ID which is not shown anymore + * sys_history uid which is selected * * @var int */ - public $lastSyslogId; - - /** - * @var string - */ - public $returnUrl; + public $lastHistoryEntry; /** * @var array @@ -81,130 +61,115 @@ class RecordHistory public $changeLog = []; /** - * @var bool - */ - public $showMarked = false; - - /** - * @var array - */ - protected $recordCache = []; - - /** + * Internal cache * @var array */ protected $pageAccessCache = []; /** + * Either "table:uid" or "table:uid:field" to know which data should be rolled back * @var string */ protected $rollbackFields = ''; /** - * @var IconFactory + * Constructor to define which element to work on - can be overriden with "setLastHistoryEntry" + * + * @param string $element in the form of "tablename:uid" + * @param string $rollbackFields */ - protected $iconFactory; + public function __construct($element = '', $rollbackFields = '') + { + $this->element = $this->sanitizeElementValue($element); + $this->rollbackFields = $this->sanitizeRollbackFieldsValue($rollbackFields); + } /** - * @var StandaloneView + * If a specific history entry is selected, then the relevant element is resolved for that. + * + * @param int $lastHistoryEntry */ - protected $view; + public function setLastHistoryEntry(int $lastHistoryEntry) + { + if ($lastHistoryEntry) { + $elementData = $this->getHistoryEntry($lastHistoryEntry); + $this->lastHistoryEntry = $lastHistoryEntry; + if (!empty($elementData) && empty($this->element)) { + $this->element = $elementData['tablename'] . ':' . $elementData['recuid']; + } + } + } /** - * Constructor for the class + * Define the maximum amount of history entries to be shown. Beware of side-effects when using + * "showSubElements" as well. + * + * @param int $maxSteps */ - public function __construct() + public function setMaxSteps(int $maxSteps) { - $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); - // GPvars: - $this->element = $this->getArgument('element'); - $this->returnUrl = $this->getArgument('returnUrl'); - $this->lastSyslogId = $this->getArgument('diff'); - $this->rollbackFields = $this->getArgument('rollbackFields'); - // Resolve sh_uid if set - $this->resolveShUid(); - - $this->view = $this->getFluidTemplateObject(); + $this->maxSteps = $maxSteps; } /** - * Main function for the listing of history. - * It detects incoming variables like element reference, history element uid etc. and renders the correct screen. + * Defines to show the history of a specific record or its subelements (when it's a page) + * as well. * - * @return string HTML content for the module + * @param bool $showSubElements */ - public function main() + public function setShowSubElements(bool $showSubElements) { - // Save snapshot - if ($this->getArgument('highlight') && !$this->getArgument('settings')) { - $this->toggleHighlight($this->getArgument('highlight')); - } - - $this->displaySettings(); + $this->showSubElements = $showSubElements; + } - if ($this->createChangeLog()) { - if ($this->rollbackFields) { - $completeDiff = $this->createMultipleDiff(); - $this->performRollback($completeDiff); - } - if ($this->lastSyslogId) { - $this->view->assign('lastSyslogId', $this->lastSyslogId); - $completeDiff = $this->createMultipleDiff(); - $this->displayMultipleDiff($completeDiff); - } - if ($this->element) { - $this->displayHistory(); - } + /** + * Creates change log including sub-elements, filling $this->changeLog + */ + public function createChangeLog() + { + if (!empty($this->element)) { + list($table, $recordUid) = explode(':', $this->element); + $this->changeLog = $this->getHistoryData($table, $recordUid, $this->showSubElements, $this->lastHistoryEntry); } + } - return $this->view->render(); + /** + * Whether rollback mode is on + * @return bool + */ + public function shouldPerformRollback() + { + return !empty($this->rollbackFields); } - /******************************* - * - * database actions - * - *******************************/ /** - * Toggles highlight state of record + * An array (0 = tablename, 1 = uid) or false if no element is set * - * @param int $uid Uid of sys_history entry + * @return array|bool */ - public function toggleHighlight($uid) + public function getElementData() { - $uid = (int)$uid; - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history'); - $row = $queryBuilder - ->select('snapshot') - ->from('sys_history') - ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))) - ->execute() - ->fetch(); + return !empty($this->element) ? explode(':', $this->element) : false; + } - if (!empty($row)) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history'); - $queryBuilder - ->update('sys_history') - ->set('snapshot', (int)!$row['snapshot']) - ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))) - ->execute(); - } + /** + * @return string named "tablename:uid" + */ + public function getElementString(): string + { + return (string)$this->element; } /** - * perform rollback - * - * @param array $diff Diff array to rollback - * @return string - * @access private + * Perform rollback via DataHandler */ - public function performRollback($diff) + public function performRollback() { - if (!$this->rollbackFields) { - return ''; + if (!$this->shouldPerformRollback()) { + return; } - $reloadPageFrame = 0; $rollbackData = explode(':', $this->rollbackFields); + $diff = $this->createMultipleDiff(); // PROCESS INSERTS AND DELETES // rewrite inserts and deletes $cmdmapArray = []; @@ -244,14 +209,10 @@ class RecordHistory // Writes the data: if ($cmdmapArray) { $tce = GeneralUtility::makeInstance(DataHandler::class); - $tce->debug = 0; - $tce->dontProcessTransformations = 1; + $tce->dontProcessTransformations = true; $tce->start([], $cmdmapArray); $tce->process_cmdmap(); unset($tce); - if (isset($cmdmapArray['pages'])) { - $reloadPageFrame = 1; - } } // PROCESS CHANGES // create an array for process_datamap @@ -278,329 +239,41 @@ class RecordHistory $data = $this->removeFilefields($rollbackData[0], $data); // Writes the data: $tce = GeneralUtility::makeInstance(DataHandler::class); - $tce->debug = 0; - $tce->dontProcessTransformations = 1; + $tce->dontProcessTransformations = true; $tce->start($data, []); $tce->process_datamap(); unset($tce); - if (isset($data['pages'])) { - $reloadPageFrame = 1; - } + // Return to normal operation - $this->lastSyslogId = false; + $this->lastHistoryEntry = false; $this->rollbackFields = ''; $this->createChangeLog(); - $this->view->assign('reloadPageFrame', $reloadPageFrame); - } - - /******************************* - * - * Display functions - * - *******************************/ - /** - * Displays settings - */ - public function displaySettings() - { - // Get current selection from UC, merge data, write it back to UC - $currentSelection = is_array($this->getBackendUser()->uc['moduleData']['history']) - ? $this->getBackendUser()->uc['moduleData']['history'] - : ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1, 'showInsertDelete' => 1]; - $currentSelectionOverride = $this->getArgument('settings'); - if ($currentSelectionOverride) { - $currentSelection = array_merge($currentSelection, $currentSelectionOverride); - $this->getBackendUser()->uc['moduleData']['history'] = $currentSelection; - $this->getBackendUser()->writeUC($this->getBackendUser()->uc); - } - // Display selector for number of history entries - $selector['maxSteps'] = [ - 10 => [ - 'value' => 10 - ], - 20 => [ - 'value' => 20 - ], - 50 => [ - 'value' => 50 - ], - 100 => [ - 'value' => 100 - ], - 999 => [ - 'value' => 'maxSteps_all' - ], - 'marked' => [ - 'value' => 'maxSteps_marked' - ] - ]; - $selector['showDiff'] = [ - 0 => [ - 'value' => 'showDiff_no' - ], - 1 => [ - 'value' => 'showDiff_inline' - ] - ]; - $selector['showSubElements'] = [ - 0 => [ - 'value' => 'no' - ], - 1 => [ - 'value' => 'yes' - ] - ]; - $selector['showInsertDelete'] = [ - 0 => [ - 'value' => 'no' - ], - 1 => [ - 'value' => 'yes' - ] - ]; - - $scriptUrl = GeneralUtility::linkThisScript(); - $languageService = $this->getLanguageService(); - - foreach ($selector as $key => $values) { - foreach ($values as $singleKey => $singleVal) { - $selector[$key][$singleKey]['scriptUrl'] = htmlspecialchars(GeneralUtility::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey)); - } - } - $this->view->assign('settings', $selector); - $this->view->assign('currentSelection', $currentSelection); - $this->view->assign('TYPO3_REQUEST_URI', htmlspecialchars(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'))); - - // set values correctly - if ($currentSelection['maxSteps'] !== 'marked') { - $this->maxSteps = $currentSelection['maxSteps'] ? (int)$currentSelection['maxSteps'] : $this->maxSteps; - } else { - $this->showMarked = true; - $this->maxSteps = false; - } - $this->showDiff = (int)$currentSelection['showDiff']; - $this->showSubElements = (int)$currentSelection['showSubElements']; - $this->showInsertDelete = (int)$currentSelection['showInsertDelete']; - - // Get link to page history if the element history is shown - $elParts = explode(':', $this->element); - if (!empty($this->element) && $elParts[0] !== 'pages') { - $this->view->assign('singleElement', 'true'); - $pid = $this->getRecord($elParts[0], $elParts[1]); - - if ($this->hasPageAccess('pages', $pid['pid'])) { - $this->view->assign('fullHistoryLink', $this->linkPage(htmlspecialchars($languageService->getLL('elementHistory_link')), ['element' => 'pages:' . $pid['pid']])); - } + if (isset($data['pages']) || isset($cmdmapArray['pages'])) { + BackendUtility::setUpdateSignal('updatePageTree'); } } - /** - * Shows the full change log - * - * @return string HTML for list, wrapped in a table. - */ - public function displayHistory() - { - if (empty($this->changeLog)) { - return ''; - } - $languageService = $this->getLanguageService(); - $lines = []; - $beUserArray = BackendUtility::getUserNames(); - - $i = 0; - - // Traverse changeLog array: - foreach ($this->changeLog as $sysLogUid => $entry) { - // stop after maxSteps - if ($this->maxSteps && $i > $this->maxSteps) { - break; - } - // Show only marked states - if (!$entry['snapshot'] && $this->showMarked) { - continue; - } - $i++; - // Build up single line - $singleLine = []; - - // Get user names - $userName = $entry['user'] ? $beUserArray[$entry['user']]['username'] : $languageService->getLL('externalChange'); - // Executed by switch-user - if (!empty($entry['originalUser'])) { - $userName .= ' (' . $languageService->getLL('viaUser') . ' ' . $beUserArray[$entry['originalUser']]['username'] . ')'; - } - $singleLine['backendUserName'] = htmlspecialchars($userName); - $singleLine['backendUserUid'] = $entry['user']; - // add user name - - // Diff link - $image = $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render(); - $singleLine['rollbackLink']= $this->linkPage($image, ['diff' => $sysLogUid]); - // remove first link - $singleLine['time'] = htmlspecialchars(BackendUtility::datetime($entry['tstamp'])); - // add time - $singleLine['age'] = htmlspecialchars(BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears'))); - // add age - - $singleLine['tableUid'] = $this->linkPage( - $this->generateTitle($entry['tablename'], $entry['recuid']), - ['element' => $entry['tablename'] . ':' . $entry['recuid']], - '', - htmlspecialchars($languageService->getLL('linkRecordHistory')) - ); - // add record UID - // Show insert/delete/diff/changed field names - if ($entry['action']) { - // insert or delete of element - $singleLine['action'] = htmlspecialchars($languageService->getLL($entry['action'])); - } else { - // Display field names instead of full diff - if (!$this->showDiff) { - // Re-write field names with labels - $tmpFieldList = explode(',', $entry['fieldlist']); - foreach ($tmpFieldList as $key => $value) { - $tmp = str_replace(':', '', htmlspecialchars($languageService->sL(BackendUtility::getItemLabel($entry['tablename'], $value)))); - if ($tmp) { - $tmpFieldList[$key] = $tmp; - } else { - // remove fields if no label available - unset($tmpFieldList[$key]); - } - } - $singleLine['fieldNames'] = htmlspecialchars(implode(',', $tmpFieldList)); - } else { - // Display diff - $diff = $this->renderDiff($entry, $entry['tablename']); - $singleLine['differences'] = $diff; - } - } - // Show link to mark/unmark state - if (!$entry['action']) { - if ($entry['snapshot']) { - $title = htmlspecialchars($languageService->getLL('unmarkState')); - $image = $this->iconFactory->getIcon('actions-unmarkstate', Icon::SIZE_SMALL)->render(); - } else { - $title = htmlspecialchars($languageService->getLL('markState')); - $image = $this->iconFactory->getIcon('actions-markstate', Icon::SIZE_SMALL)->render(); - } - $singleLine['markState'] = $this->linkPage($image, ['highlight' => $entry['uid']], '', $title); - } else { - $singleLine['markState'] = ''; - } - // put line together - $lines[] = $singleLine; - } - $this->view->assign('history', $lines); - - if ($this->lastSyslogId) { - $this->view->assign('fullViewLink', $this->linkPage(htmlspecialchars($languageService->getLL('fullView')), ['diff' => ''])); - } - } - - /** - * Displays a diff over multiple fields including rollback links - * - * @param array $diff Difference array - */ - public function displayMultipleDiff($diff) - { - // Get all array keys needed - $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData'])); - $arrayKeys = array_unique($arrayKeys); - $languageService = $this->getLanguageService(); - if ($arrayKeys) { - $lines = []; - foreach ($arrayKeys as $key) { - $singleLine = []; - $elParts = explode(':', $key); - // Turn around diff because it should be a "rollback preview" - if ((int)$diff['insertsDeletes'][$key] === 1) { - // insert - $singleLine['insertDelete'] = 'delete'; - } elseif ((int)$diff['insertsDeletes'][$key] === -1) { - $singleLine['insertDelete'] = 'insert'; - } - // Build up temporary diff array - // turn around diff because it should be a "rollback preview" - if ($diff['newData'][$key]) { - $tmpArr['newRecord'] = $diff['oldData'][$key]; - $tmpArr['oldRecord'] = $diff['newData'][$key]; - $singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], $elParts[1]); - } - $elParts = explode(':', $key); - $singleLine['revertRecordLink'] = $this->createRollbackLink($key, htmlspecialchars($languageService->getLL('revertRecord')), 1); - $singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]); - $lines[] = $singleLine; - } - $this->view->assign('revertAllLink', $this->createRollbackLink('ALL', htmlspecialchars($languageService->getLL('revertAll')), 0)); - $this->view->assign('multipleDiff', $lines); - } - } - - /** - * Renders HTML table-rows with the comparison information of an sys_history entry record - * - * @param array $entry sys_history entry record. - * @param string $table The table name - * @param int $rollbackUid If set to UID of record, display rollback links - * @return string|NULL HTML table - * @access private - */ - public function renderDiff($entry, $table, $rollbackUid = 0) - { - $lines = []; - if (is_array($entry['newRecord'])) { - /* @var DiffUtility $diffUtility */ - $diffUtility = GeneralUtility::makeInstance(DiffUtility::class); - $diffUtility->stripTags = false; - $fieldsToDisplay = array_keys($entry['newRecord']); - $languageService = $this->getLanguageService(); - foreach ($fieldsToDisplay as $fN) { - if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') { - // Create diff-result: - $diffres = $diffUtility->makeDiffDisplay( - BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true), - BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true) - ); - $lines[] = [ - 'title' => ($rollbackUid ? $this->createRollbackLink(($table . ':' . $rollbackUid . ':' . $fN), htmlspecialchars($languageService->getLL('revertField')), 2) : '') . ' - ' . htmlspecialchars($languageService->sL(BackendUtility::getItemLabel($table, $fN))), - 'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres)) - ]; - } - } - } - if ($lines) { - return $lines; - } - // error fallback - return null; - } - /******************************* * * build up history * *******************************/ + /** * Creates a diff between the current version of the records and the selected version * - * @return array Diff for many elements, 0 if no changelog is found + * @return array Diff for many elements */ - public function createMultipleDiff() + public function createMultipleDiff(): array { $insertsDeletes = []; $newArr = []; $differences = []; - if (!$this->changeLog) { - return 0; - } // traverse changelog array foreach ($this->changeLog as $value) { $field = $value['tablename'] . ':' . $value['recuid']; // inserts / deletes - if ($value['action']) { + if ((int)$value['actiontype'] !== RecordHistoryStore::ACTION_MODIFY) { if (!$insertsDeletes[$field]) { $insertsDeletes[$field] = 0; } @@ -646,21 +319,19 @@ class RecordHistory } /** - * Creates change log including sub-elements, filling $this->changeLog + * Fetches the history data of a record + includes subelements if this is from a page * - * @return int + * @param string $table + * @param int $uid + * @param bool $includeSubentries + * @param int $lastHistoryEntry the highest entry to be evaluated + * @return array */ - public function createChangeLog() + public function getHistoryData(string $table, int $uid, bool $includeSubentries = null, int $lastHistoryEntry = null): array { - $elParts = explode(':', $this->element); - - if (empty($this->element)) { - return 0; - } - - $changeLog = $this->getHistoryData($elParts[0], $elParts[1]); + $changeLog = $this->getHistoryDataForRecord($table, $uid, $lastHistoryEntry); // get history of tables of this page and merge it into changelog - if ($elParts[0] === 'pages' && $this->showSubElements && $this->hasPageAccess('pages', $elParts[1])) { + if ($table === 'pages' && $includeSubentries && $this->hasPageAccess('pages', $uid)) { foreach ($GLOBALS['TCA'] as $tablename => $value) { // check if there are records on the page $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tablename); @@ -672,7 +343,7 @@ class RecordHistory ->where( $queryBuilder->expr()->eq( 'pid', - $queryBuilder->createNamedParameter($elParts[1], \PDO::PARAM_INT) + $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) ) ) ->execute(); @@ -681,7 +352,7 @@ class RecordHistory } foreach ($rows as $row) { // if there is history data available, merge it into changelog - $newChangeLog = $this->getHistoryData($tablename, $row['uid']); + $newChangeLog = $this->getHistoryDataForRecord($tablename, $row['uid'], $lastHistoryEntry); if (is_array($newChangeLog) && !empty($newChangeLog)) { foreach ($newChangeLog as $key => $newChangeLogEntry) { $changeLog[$key] = $newChangeLogEntry; @@ -690,12 +361,16 @@ class RecordHistory } } } - if (!$changeLog) { + usort($changeLog, function ($a, $b) { + if ($a['tstamp'] < $b['tstamp']) { + return 1; + } + if ($a['tstamp'] > $b['tstamp']) { + return -1; + } return 0; - } - krsort($changeLog); - $this->changeLog = $changeLog; - return 1; + }); + return $changeLog; } /** @@ -703,118 +378,17 @@ class RecordHistory * * @param string $table DB table name * @param int $uid UID of record - * @return array|int Array of history data of the record or 0 if no history could be fetched + * @param int $lastHistoryEntry the highest entry to be fetched + * @return array Array of history data of the record */ - public function getHistoryData($table, $uid) + public function getHistoryDataForRecord(string $table, int $uid, int $lastHistoryEntry = null): array { if (empty($GLOBALS['TCA'][$table]) || !$this->hasTableAccess($table) || !$this->hasPageAccess($table, $uid)) { - // error fallback - return 0; - } - // If table is found in $GLOBALS['TCA']: - $uid = $this->resolveElement($table, $uid); - // Selecting the $this->maxSteps most recent states: - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history'); - $rows = $queryBuilder - ->select('sys_history.*', 'sys_log.userid', 'sys_log.log_data') - ->from('sys_history') - ->from('sys_log') - ->where( - $queryBuilder->expr()->eq( - 'sys_history.sys_log_uid', - $queryBuilder->quoteIdentifier('sys_log.uid') - ), - $queryBuilder->expr()->eq( - 'sys_history.tablename', - $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR) - ), - $queryBuilder->expr()->eq( - 'sys_history.recuid', - $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) - ) - ) - ->orderBy('sys_log.uid', 'DESC') - ->setMaxResults((int)$this->maxSteps) - ->execute() - ->fetchAll(); - - $changeLog = []; - if (!empty($rows)) { - // Traversing the result, building up changesArray / changeLog: - foreach ($rows as $row) { - // Only history until a certain syslog ID needed - if ($this->lastSyslogId && $row['sys_log_uid'] < $this->lastSyslogId) { - continue; - } - $hisDat = unserialize($row['history_data']); - $logData = unserialize($row['log_data']); - if (is_array($hisDat['newRecord']) && is_array($hisDat['oldRecord'])) { - // Add information about the history to the changeLog - $hisDat['uid'] = $row['uid']; - $hisDat['tstamp'] = $row['tstamp']; - $hisDat['user'] = $row['userid']; - $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']); - $hisDat['snapshot'] = $row['snapshot']; - $hisDat['fieldlist'] = $row['fieldlist']; - $hisDat['tablename'] = $row['tablename']; - $hisDat['recuid'] = $row['recuid']; - $changeLog[$row['sys_log_uid']] = $hisDat; - } else { - debug('ERROR: [getHistoryData]'); - // error fallback - return 0; - } - } + return []; } - // SELECT INSERTS/DELETES - if ($this->showInsertDelete) { - // Select most recent inserts and deletes // WITHOUT snapshots - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log'); - $result = $queryBuilder - ->select('uid', 'userid', 'action', 'tstamp', 'log_data') - ->from('sys_log') - ->where( - $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)), - $queryBuilder->expr()->orX( - $queryBuilder->expr()->eq('action', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)), - $queryBuilder->expr()->eq('action', $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)) - ), - $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)), - $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)) - ) - ->orderBy('uid', 'DESC') - ->setMaxResults((int)$this->maxSteps) - ->execute(); - // If none are found, nothing more to do - if ($result->rowCount() === 0) { - return $changeLog; - } - foreach ($result as $row) { - if ($this->lastSyslogId && $row['uid'] < $this->lastSyslogId) { - continue; - } - $hisDat = []; - $logData = unserialize($row['log_data']); - switch ($row['action']) { - case 1: - // Insert - $hisDat['action'] = 'insert'; - break; - case 3: - // Delete - $hisDat['action'] = 'delete'; - break; - } - $hisDat['tstamp'] = $row['tstamp']; - $hisDat['user'] = $row['userid']; - $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']); - $hisDat['tablename'] = $table; - $hisDat['recuid'] = $uid; - $changeLog[$row['uid']] = $hisDat; - } - } - return $changeLog; + $uid = $this->resolveElement($table, $uid); + return $this->findEventsForRecord($table, $uid, ($this->maxSteps ?: null), $lastHistoryEntry); } /******************************* @@ -822,58 +396,6 @@ class RecordHistory * Various helper functions * *******************************/ - /** - * Generates the title and puts the record title behind - * - * @param string $table - * @param string $uid - * @return string - */ - public function generateTitle($table, $uid) - { - $out = $table . ':' . $uid; - if ($labelField = $GLOBALS['TCA'][$table]['ctrl']['label']) { - $record = $this->getRecord($table, $uid); - $out .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')'; - } - return $out; - } - - /** - * Creates a link for the rollback - * - * @param string $key Parameter which is set to rollbackFields - * @param string $alt Optional, alternative label and title tag of image - * @param int $type Optional, type of rollback: 0 - ALL; 1 - element; 2 - field - * @return string HTML output - */ - public function createRollbackLink($key, $alt = '', $type = 0) - { - return $this->linkPage('<span class="btn btn-default" style="margin-right: 5px;">' . $alt . '</span>', ['rollbackFields' => $key]); - } - - /** - * Creates a link to the same page. - * - * @param string $str String to wrap in <a> tags (must be htmlspecialchars()'ed prior to calling function) - * @param array $inparams Array of key/value pairs to override the default values with. - * @param string $anchor Possible anchor value. - * @param string $title Possible title. - * @return string Link. - * @access private - */ - public function linkPage($str, $inparams = [], $anchor = '', $title = '') - { - // Setting default values based on GET parameters: - $params['element'] = $this->element; - $params['returnUrl'] = $this->returnUrl; - $params['diff'] = $this->lastSyslogId; - // Merging overriding values: - $params = array_merge($params, $inparams); - // Make the link: - $link = BackendUtility::getModuleUrl('record_history', $params) . ($anchor ? '#' . $anchor : ''); - return '<a href="' . htmlspecialchars($link) . '"' . ($title ? ' title="' . $title . '"' : '') . '>' . $str . '</a>'; - } /** * Will traverse the field names in $dataArray and look in $GLOBALS['TCA'] if the fields are of types which cannot @@ -884,7 +406,7 @@ class RecordHistory * @return array The modified data array * @access private */ - public function removeFilefields($table, $dataArray) + protected function removeFilefields($table, $dataArray) { if ($GLOBALS['TCA'][$table]) { foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) { @@ -903,38 +425,93 @@ class RecordHistory * @param int $uid UID of record * @return int converted UID of record */ - public function resolveElement($table, $uid) + protected function resolveElement(string $table, int $uid): int { - if (isset($GLOBALS['TCA'][$table])) { - if ($workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $uid, 'uid')) { - $uid = $workspaceVersion['uid']; - } + if (isset($GLOBALS['TCA'][$table]) + && $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $uid, 'uid')) { + $uid = $workspaceVersion['uid']; } return $uid; } /** - * Resolve sh_uid (used from log) + * Resolve tablename + record uid from sys_history UID + * + * @param int $lastHistoryEntry + * @return array */ - public function resolveShUid() + public function getHistoryEntry(int $lastHistoryEntry): array { - $shUid = $this->getArgument('sh_uid'); - if (empty($shUid)) { - return; - } - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history'); + $queryBuilder = $this->getQueryBuilder(); $record = $queryBuilder - ->select('*') + ->select('uid', 'tablename', 'recuid') ->from('sys_history') - ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($shUid, \PDO::PARAM_INT))) + ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($lastHistoryEntry, \PDO::PARAM_INT))) ->execute() ->fetch(); if (empty($record)) { - return; + return []; + } + + return $record; + } + + /** + * Queries the DB and prepares the results + * Resolving a WSOL of the UID and checking permissions is explicitly not part of this method + * + * @param string $table + * @param int $uid + * @param int $limit + * @param int $minimumUid + * @return array + */ + public function findEventsForRecord(string $table, int $uid, int $limit = 0, int $minimumUid = null): array + { + $events = []; + $queryBuilder = $this->getQueryBuilder(); + $queryBuilder + ->select('*') + ->from('sys_history') + ->where( + $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)), + $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)) + ); + + if ($limit) { + $queryBuilder->setMaxResults($limit); + } + + if ($minimumUid) { + $queryBuilder->andWhere($queryBuilder->expr()->gte('uid', $queryBuilder->createNamedParameter($minimumUid, \PDO::PARAM_INT))); } - $this->element = $record['tablename'] . ':' . $record['recuid']; - $this->lastSyslogId = $record['sys_log_uid'] - 1; + + $result = $queryBuilder->orderBy('tstamp', 'DESC')->execute(); + while ($row = $result->fetch()) { + $identifier = (int)$row['uid']; + if ((int)$row['actiontype'] === RecordHistoryStore::ACTION_ADD || (int)$row['actiontype'] === RecordHistoryStore::ACTION_UNDELETE) { + $row['action'] = 'insert'; + } + if ((int)$row['actiontype'] === RecordHistoryStore::ACTION_DELETE) { + $row['action'] = 'delete'; + } + if (strpos($row['history_data'], 'a') === 0) { + // 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; } /** @@ -951,7 +528,7 @@ class RecordHistory if ($table === 'pages') { $pageId = $uid; } else { - $record = $this->getRecord($table, $uid); + $record = BackendUtility::getRecord($table, $uid, '*', '', false); $pageId = $record['pid']; } @@ -966,109 +543,62 @@ class RecordHistory } /** - * Determines whether user has access to a table. + * Fetches GET/POST arguments and sanitizes the values for + * the expected disposal. Invalid values will be converted + * to an empty string. * - * @param string $table - * @return bool + * @param string $value the value of the element value + * @return array|string|int */ - protected function hasTableAccess($table) + protected function sanitizeElementValue($value) { - return $this->getBackendUser()->check('tables_select', $table); + if ($value !== '' && !preg_match('#^[a-z0-9_.]+:[0-9]+$#i', $value)) { + return ''; + } + return $value; } /** - * Gets a database record. + * Evaluates if the rollback field is correct * - * @param string $table - * @param int $uid - * @return array|NULL + * @param string $value + * @return string */ - protected function getRecord($table, $uid) + protected function sanitizeRollbackFieldsValue($value) { - if (!isset($this->recordCache[$table][$uid])) { - $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false); + if ($value !== '' && !preg_match('#^[a-z0-9_.]+(:[0-9]+(:[a-z0-9_.]+)?)?$#i', $value)) { + return ''; } - return $this->recordCache[$table][$uid]; + return $value; } /** - * Gets the current backend user. + * Determines whether user has access to a table. * - * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication + * @param string $table + * @return bool */ - protected function getBackendUser() + protected function hasTableAccess($table) { - return $GLOBALS['BE_USER']; + return $this->getBackendUser()->check('tables_select', $table); } /** - * Fetches GET/POST arguments and sanitizes the values for - * the expected disposal. Invalid values will be converted - * to an empty string. + * Gets the current backend user. * - * @param string $name Name of the argument - * @return array|string|int - */ - protected function getArgument($name) - { - $value = GeneralUtility::_GP($name); - - switch ($name) { - case 'element': - if ($value !== '' && !preg_match('#^[a-z0-9_.]+:[0-9]+$#i', $value)) { - $value = ''; - } - break; - case 'rollbackFields': - case 'revert': - if ($value !== '' && !preg_match('#^[a-z0-9_.]+(:[0-9]+(:[a-z0-9_.]+)?)?$#i', $value)) { - $value = ''; - } - break; - case 'returnUrl': - $value = GeneralUtility::sanitizeLocalUrl($value); - break; - case 'diff': - case 'highlight': - case 'sh_uid': - $value = (int)$value; - break; - case 'settings': - if (!is_array($value)) { - $value = []; - } - break; - default: - $value = ''; - } - - return $value; - } - - /** - * @return \TYPO3\CMS\Core\Localization\LanguageService + * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication */ - protected function getLanguageService() + protected function getBackendUser() { - return $GLOBALS['LANG']; + return $GLOBALS['BE_USER']; } /** - * returns a new standalone view, shorthand function - * - * @return StandaloneView + * @return QueryBuilder */ - protected function getFluidTemplateObject() + protected function getQueryBuilder(): QueryBuilder { - /** @var StandaloneView $view */ - $view = GeneralUtility::makeInstance(StandaloneView::class); - $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]); - $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]); - $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]); - - $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html')); - - $view->getRequest()->setControllerExtensionName('Backend'); - return $view; + return GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_history'); } } diff --git a/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/Diff.html b/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/Diff.html index 65a4861bad76..37c3fa09b1ce 100644 --- a/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/Diff.html +++ b/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/Diff.html @@ -2,11 +2,14 @@ <f:for each="{differences}" as="differencesItem" key="key"> <div class="diff-item"> <div class="diff-item-title"> - <f:format.raw>{differencesItem.title}</f:format.raw> + <f:if condition="{rollbackUrl}"> + <a href="{rollbackUrl}" class="btn btn-default" style="margin-right: 5px;">{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:revertField')}</a> + </f:if> + {differencesItem.link} {differencesItem.title} </div> - <div class="diff-item-result"> + <div class="diff-item-result"><f:spaceless> <f:format.raw>{differencesItem.result}</f:format.raw> - </div> + </f:spaceless></div> </div> </f:for> </div> diff --git a/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/History.html b/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/History.html index aa63014ef05d..dc39b3beb7fb 100644 --- a/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/History.html +++ b/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/History.html @@ -12,47 +12,57 @@ <th>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:user')}</th> <th>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:tableUid')}</th> <th>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:differences')}</th> - <th> </th> </tr> </thead> <tbody> - <f:for each="{history}" as="historyRow" key="key"> + <f:for each="{history}" as="historyRow"> <tr> <td><span><span title="{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:sumUpChanges')}"> - {historyRow.rollbackLink -> f:format.raw()} + <a href="{historyRow.diffUrl}"><core:icon identifier="actions-document-history-open" /></a> </span></span></td> <td>{historyRow.time}</td> <td>{historyRow.age}</td> <td> <be:avatar backendUser="{historyRow.backendUserUid}"/> - {historyRow.backendUserName} + <f:if condition="{historyRow.backendUserUid}"> + <f:then> + {historyRow.backendUserName} + </f:then> + <f:else> + {f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:externalChange')} + </f:else> + </f:if> + <f:if condition="{historyRow.originalBackendUserName}"> ({f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:viaUser')} {historyRow.originalBackendUserName})</f:if> </td> <td> - {historyRow.tableUid -> f:format.raw()} + <a href="{elementUrl}" title="{f:translate('LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:linkRecordHistory')}">{historyRow.title}</a> </td> <td> - <f:if condition="{historyRow.action}"> - <strong> - {historyRow.action -> f:format.raw()} - </strong> - </f:if> + <f:switch expression="{historyRow.actiontype}"> + <f:case value="1"> + <strong>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:insert')}</strong> + </f:case> + <f:case value="4"> + <strong>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:delete')}</strong> + </f:case> + <f:case value="5"> + <strong>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:insert')}</strong> + </f:case> + </f:switch> <f:if condition="{historyRow.fieldNames}"> - {historyRow.fieldNames -> f:format.raw()} + {historyRow.fieldNames} </f:if> <f:if condition="{historyRow.differences}"> <f:render partial="RecordHistory/Diff" arguments="{differences: historyRow.differences}"/> </f:if> </td> - <td> - {historyRow.markState -> f:format.raw()} - </td> </tr> </f:for> </tbody> </table> - <f:if condition="{fullViewLink}"> + <f:if condition="{fullViewUrl}"> <br/> - <f:format.raw><span class="btn btn-default">{fullViewLink}</span></f:format.raw> + <a href="{fullViewUrl}" class="btn btn-default">{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:fullView')}</a> </f:if> <br/> <br/> diff --git a/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/MultipleDiff.html b/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/MultipleDiff.html index af6ef5ac08f5..57aaa3bc3d78 100644 --- a/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/MultipleDiff.html +++ b/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/MultipleDiff.html @@ -1,13 +1,13 @@ <h2>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:mergedDifferences')}</h2> <div> - <f:if condition="{revertAllLink}"> + <f:if condition="{revertAllUrl}"> <f:then> - <f:format.raw>{revertAllLink}</f:format.raw> + <a href="{revertAllUrl}" class="btn btn-default" style="margin-right: 5px;">{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:revertAll')}</a> <div style="padding-left:10px;border-left:5px solid darkgray;border-bottom:1px dotted darkgray;padding-bottom:2px;"> <f:for each="{multipleDiff}" as="historyRow" key="key"> <h3> - <f:format.raw>{historyRow.revertRecordLink}</f:format.raw> + <a href="{historyRow.revertRecordUrl}" class="btn btn-default" style="margin-right: 5px;">{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:revertRecord')}</a> {historyRow.title} </h3> <div> diff --git a/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/Settings.html b/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/Settings.html index 509aae063f2e..dcb69d42c311 100644 --- a/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/Settings.html +++ b/typo3/sysext/backend/Resources/Private/Partials/RecordHistory/Settings.html @@ -1,11 +1,10 @@ <div> <f:if condition="{singleElement}"> - <strong>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:elementHistory')}</strong><br/> - <f:if condition="{fullHistoryLink}"> - <span class="btn btn-default" style="margin-bottom: 5px;"><f:format.raw>{fullHistoryLink}</f:format.raw></span> + <h3>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:elementHistory')}</h3> + <f:if condition="{fullHistoryUrl}"> + <a href="{fullHistoryUrl}" class="btn btn-default" style="margin-bottom: 5px;">{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:elementHistory_link')}</a> </f:if> </f:if> - <a name="settings_head"></a> <form name="settings" action="{TYPO3_REQUEST_URI}" method="post"> <div class="row"> <div class="col-sm-12 col-md-6 col-lg-4"> diff --git a/typo3/sysext/backend/Resources/Private/Templates/RecordHistory/Main.html b/typo3/sysext/backend/Resources/Private/Templates/RecordHistory/Main.html index dbf24bdf22db..503ae011739a 100644 --- a/typo3/sysext/backend/Resources/Private/Templates/RecordHistory/Main.html +++ b/typo3/sysext/backend/Resources/Private/Templates/RecordHistory/Main.html @@ -1,14 +1,6 @@ -<f:if condition="{reloadPageFrame}"> - <script type="text/javascript"> - /*<![CDATA[*/ - if (top.nav_frame && top.nav_frame.refresh_nav) { - top.nav_frame.refresh_nav(); - } - /*]]>*/ - </script> -</f:if> +<h1>{f:translate(key: 'LLL:EXT:backend/Resources/Private/Language/locallang_show_rechis.xlf:title')}</h1> <f:render partial="RecordHistory/Settings" arguments="{_all}" /> -<f:if condition="{lastSyslogId}"> +<f:if condition="{showDifferences}"> <f:render partial="RecordHistory/MultipleDiff" arguments="{_all}" /> </f:if> <f:render partial="RecordHistory/History" arguments="{_all}" /> diff --git a/typo3/sysext/belog/Classes/Domain/Model/HistoryEntry.php b/typo3/sysext/belog/Classes/Domain/Model/HistoryEntry.php deleted file mode 100644 index 348901cba7cc..000000000000 --- a/typo3/sysext/belog/Classes/Domain/Model/HistoryEntry.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php -namespace TYPO3\CMS\Belog\Domain\Model; - -/* - * 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! - */ - -/** - * Stub model for sys history - only properties required for belog module are added currently - */ -class HistoryEntry extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity -{ - /** - * List of changed fields - * - * @var string - */ - protected $fieldlist = ''; - - /** - * Set list of changed fields - * - * @param string $fieldlist - */ - public function setFieldlist($fieldlist) - { - // @todo think about exploding this to an array - $this->fieldlist = $fieldlist; - } - - /** - * Get field list - * - * @return string - */ - public function getFieldlist() - { - return $this->fieldlist; - } -} diff --git a/typo3/sysext/belog/Classes/Domain/Repository/HistoryEntryRepository.php b/typo3/sysext/belog/Classes/Domain/Repository/HistoryEntryRepository.php deleted file mode 100644 index d0764b9f1eae..000000000000 --- a/typo3/sysext/belog/Classes/Domain/Repository/HistoryEntryRepository.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php -namespace TYPO3\CMS\Belog\Domain\Repository; - -/* - * 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! - */ - -/** - * Find system history entries - */ -class HistoryEntryRepository extends \TYPO3\CMS\Extbase\Persistence\Repository -{ - /** - * Initializes the repository. - */ - public function initializeObject() - { - /** @var $querySettings \TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface */ - $querySettings = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface::class); - $querySettings->setRespectStoragePage(false); - $this->setDefaultQuerySettings($querySettings); - } -} diff --git a/typo3/sysext/belog/Classes/ViewHelpers/FormatDetailsViewHelper.php b/typo3/sysext/belog/Classes/ViewHelpers/FormatDetailsViewHelper.php index 610439c8ca43..2b7787bfd41e 100644 --- a/typo3/sysext/belog/Classes/ViewHelpers/FormatDetailsViewHelper.php +++ b/typo3/sysext/belog/Classes/ViewHelpers/FormatDetailsViewHelper.php @@ -62,7 +62,9 @@ class FormatDetailsViewHelper extends AbstractViewHelper $substitutes = self::stripPathFromFilenames($substitutes); } // Substitute - $detailString = vsprintf($detailString, $substitutes); + if (!empty($substitutes)) { + $detailString = vsprintf($detailString, $substitutes); + } // Remove possible pending other %s $detailString = str_replace('%s', '', $detailString); return $detailString; diff --git a/typo3/sysext/belog/Classes/ViewHelpers/HistoryEntryViewHelper.php b/typo3/sysext/belog/Classes/ViewHelpers/HistoryEntryViewHelper.php deleted file mode 100644 index 238e434d8f80..000000000000 --- a/typo3/sysext/belog/Classes/ViewHelpers/HistoryEntryViewHelper.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php -namespace TYPO3\CMS\Belog\ViewHelpers; - -/* - * 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\Backend\Utility\BackendUtility; -use TYPO3\CMS\Belog\Domain\Model\HistoryEntry; -use TYPO3\CMS\Belog\Domain\Repository\HistoryEntryRepository; -use TYPO3\CMS\Core\Imaging\Icon; -use TYPO3\CMS\Core\Imaging\IconFactory; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Extbase\Object\ObjectManager; -use TYPO3\CMS\Extbase\Utility\LocalizationUtility; -use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext; -use TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper; -use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; -use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic; - -/** - * Get history entry from for log entry - * @internal - */ -class HistoryEntryViewHelper extends AbstractViewHelper -{ - use CompileWithRenderStatic; - - /** - * As this ViewHelper renders HTML, the output must not be escaped. - * - * @var bool - */ - protected $escapeOutput = false; - - /** - * Initializes the arguments - */ - public function initializeArguments() - { - parent::initializeArguments(); - $this->registerArgument('uid', 'int', 'Uid of the log entry', true); - } - - /** - * Get system history record - * - * @param array $arguments - * @param \Closure $renderChildrenClosure - * @param RenderingContextInterface $renderingContext - * - * @return string Formatted history entry if one exists, else empty string - * @throws \InvalidArgumentException - */ - public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) - { - if (!$renderingContext instanceof RenderingContext) { - throw new \InvalidArgumentException('The given rendering context is not of type "TYPO3\CMS\Fluid\Core\Rendering\RenderingContext"', 1468363945); - } - /** @var \TYPO3\CMS\Extbase\Object\ObjectManager $objectManager */ - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); - /** @var \TYPO3\CMS\Belog\Domain\Repository\HistoryEntryRepository $historyEntryRepository */ - $historyEntryRepository = $objectManager->get(HistoryEntryRepository::class); - /** @var \TYPO3\CMS\Belog\Domain\Model\HistoryEntry $historyEntry */ - $historyEntry = $historyEntryRepository->findOneBySysLogUid($arguments['uid']); - $controllerContext = $renderingContext->getControllerContext(); - /** @var IconFactory $iconFactory */ - $iconFactory = GeneralUtility::makeInstance(IconFactory::class); - - if (!$historyEntry instanceof HistoryEntry) { - return ''; - } - $historyLabel = LocalizationUtility::translate( - 'changesInFields', - $controllerContext->getRequest()->getControllerExtensionName(), - [$historyEntry->getFieldlist()] - ); - $titleLable = LocalizationUtility::translate( - 'showHistory', - $controllerContext->getRequest()->getControllerExtensionName() - ); - $historyIcon = $iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render(); - $historyHref = BackendUtility::getModuleUrl( - 'record_history', - [ - 'sh_uid' => $historyEntry->getUid(), - 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI'), - ] - ); - $historyLink = '<a href="' . htmlspecialchars($historyHref) . '" title="' . htmlspecialchars($titleLable) . '">' . $historyIcon . '</a>'; - return htmlspecialchars($historyLabel) . ' ' . $historyLink; - } -} diff --git a/typo3/sysext/belog/Configuration/TypoScript/setup.txt b/typo3/sysext/belog/Configuration/TypoScript/setup.txt index be000fc42e36..9e515186d209 100644 --- a/typo3/sysext/belog/Configuration/TypoScript/setup.txt +++ b/typo3/sysext/belog/Configuration/TypoScript/setup.txt @@ -20,11 +20,6 @@ module.tx_belog { tableName = sys_workspace } } - TYPO3\CMS\Belog\Domain\Model\HistoryEntry { - mapping { - tableName = sys_history - } - } } settings { diff --git a/typo3/sysext/belog/Resources/Private/Partials/Content/LogEntries.html b/typo3/sysext/belog/Resources/Private/Partials/Content/LogEntries.html index c47b801e999c..33b3a0f87219 100644 --- a/typo3/sysext/belog/Resources/Private/Partials/Content/LogEntries.html +++ b/typo3/sysext/belog/Resources/Private/Partials/Content/LogEntries.html @@ -203,7 +203,11 @@ </td> <td class="col-word-break"> <belog:formatDetails logEntry="{logItem}"/> - <belog:historyEntry uid="{logItem.uid}"/> + <f:if condition="{logItem.logData.history}"> + <a href="{be:moduleLink(route: 'record_history', arguments: '{historyEntry: logItem.logData.history}')}" title="{f:translate(key: 'showHistory')}"> + <core:icon identifier="actions-document-history-open" /> + </a> + </f:if> <f:if condition="{logItem.detailsNumber} > 0"> (msg#{logItem.type}.{logItem.action}.{logItem.detailsNumber}) </f:if> diff --git a/typo3/sysext/belog/Tests/Unit/Domain/Repository/HistoryEntryRepositoryTest.php b/typo3/sysext/belog/Tests/Unit/Domain/Repository/HistoryEntryRepositoryTest.php deleted file mode 100644 index 4167b9bb83d4..000000000000 --- a/typo3/sysext/belog/Tests/Unit/Domain/Repository/HistoryEntryRepositoryTest.php +++ /dev/null @@ -1,47 +0,0 @@ -<?php -namespace TYPO3\CMS\Belog\Tests\Unit\Domain\Repository; - -/* - * 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! - */ - -/** - * Test case - */ -class HistoryEntryRepositoryTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase -{ - /** - * @var \TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings|\TYPO3\TestingFramework\Core\AccessibleObjectInterface - */ - protected $querySettings = null; - - protected function setUp() - { - $this->querySettings = $this->getMockBuilder(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface::class)->getMock(); - $this->objectManager = $this->getMockBuilder(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface::class)->getMock(); - $this->objectManager->expects($this->any())->method('get')->with(\TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface::class)->will($this->returnValue($this->querySettings)); - } - - /** - * @test - */ - public function initializeObjectSetsRespectStoragePidToFalse() - { - $this->querySettings->expects($this->atLeastOnce())->method('setRespectStoragePage')->with(false); - $fixture = $this->getMockBuilder(\TYPO3\CMS\Belog\Domain\Repository\HistoryEntryRepository::class) - ->setMethods(['setDefaultQuerySettings']) - ->setConstructorArgs([$this->objectManager]) - ->getMock(); - $fixture->expects($this->once())->method('setDefaultQuerySettings')->with($this->querySettings); - $fixture->initializeObject(); - } -} diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index bb61317aadf7..a3277ffe7a8d 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -33,6 +33,7 @@ use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface use TYPO3\CMS\Core\Database\ReferenceIndex; use TYPO3\CMS\Core\Database\RelationHandler; use TYPO3\CMS\Core\DataHandling\Localization\DataMapProcessor; +use TYPO3\CMS\Core\History\RecordHistoryStore; use TYPO3\CMS\Core\Html\RteHtmlParser; use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessageService; @@ -4459,6 +4460,8 @@ class DataHandler $hookObj->moveRecord_firstElementPostProcess($table, $uid, $destPid, $moveRec, $updateFields, $this); } } + + $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields]); if ($this->enableLogging) { // Logging... $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']); @@ -4516,6 +4519,7 @@ class DataHandler $hookObj->moveRecord_afterAnotherElementPostProcess($table, $uid, $destPid, $origDestPid, $moveRec, $updateFields, $this); } } + $this->getRecordHistoryStore()->moveRecord($table, $uid, ['oldPageId' => $propArr['pid'], 'newPageId' => $destPid, 'oldData' => $propArr, 'newData' => $updateFields]); if ($this->enableLogging) { // Logging... $oldpagePropArr = $this->getRecordProperties('pages', $propArr['pid']); @@ -5269,6 +5273,14 @@ class DataHandler $this->log($table, $uid, $state, 0, 100, $databaseErrorMessage); } } + + // Add history entry + if ($undeleteRecord) { + $this->getRecordHistoryStore()->undeleteRecord($table, $uid); + } else { + $this->getRecordHistoryStore()->deleteRecord($table, $uid); + } + // Update reference index: $this->updateRefIndex($table, $uid); @@ -7077,6 +7089,11 @@ class DataHandler if ($updateErrorMessage === '') { // Update reference index: $this->updateRefIndex($table, $id); + // Set History data + $historyEntryId = 0; + if (isset($this->historyRecords[$table . ':' . $id])) { + $historyEntryId = $this->getRecordHistoryStore()->modifyRecord($table, $id, $this->historyRecords[$table . ':' . $id]); + } if ($this->enableLogging) { if ($this->checkStoredRecords) { $newRow = $this->checkStoredRecord($table, $id, $fieldArray, 2); @@ -7086,9 +7103,7 @@ class DataHandler } // Set log entry: $propArr = $this->getRecordPropertiesFromRow($table, $newRow); - $theLogId = $this->log($table, $id, 2, $propArr['pid'], 0, 'Record \'%s\' (%s) was updated.' . ($propArr['_ORIG_pid'] == -1 ? ' (Offline version).' : ' (Online).'), 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']); - // Set History data: - $this->setHistory($table, $id, $theLogId); + $this->log($table, $id, 2, $propArr['pid'], 0, 'Record \'%s\' (%s) was updated.' . ($propArr['_ORIG_pid'] == -1 ? ' (Offline version).' : ' (Online).'), 10, [$propArr['header'], $table . ':' . $id, 'history' => $historyEntryId], $propArr['event_pid']); } // Clear cache for relevant pages: $this->registerRecordIdForPageCacheClearing($table, $id); @@ -7177,6 +7192,10 @@ class DataHandler } // Update reference index: $this->updateRefIndex($table, $id); + + // Store in history + $this->getRecordHistoryStore()->addRecord($table, $id, $newRow); + if ($newVersion) { if ($this->enableLogging) { $propArr = $this->getRecordPropertiesFromRow($table, $newRow); @@ -7270,26 +7289,37 @@ class DataHandler /** * Setting sys_history record, based on content previously set in $this->historyRecords[$table . ':' . $id] (by compareFieldArrayWithCurrentAndUnset()) * + * This functionality is now moved into the RecordHistoryStore and can be used instead. + * * @param string $table Table name * @param int $id Record ID * @param int $logId Log entry ID, important for linking between log and history views */ public function setHistory($table, $id, $logId) { - if (isset($this->historyRecords[$table . ':' . $id]) && (int)$logId > 0) { - $fields_values = []; - $fields_values['history_data'] = serialize($this->historyRecords[$table . ':' . $id]); - $fields_values['fieldlist'] = implode(',', array_keys($this->historyRecords[$table . ':' . $id]['newRecord'])); - $fields_values['tstamp'] = $GLOBALS['EXEC_TIME']; - $fields_values['tablename'] = $table; - $fields_values['recuid'] = $id; - $fields_values['sys_log_uid'] = $logId; - GeneralUtility::makeInstance(ConnectionPool::class) - ->getConnectionForTable('sys_history') - ->insert('sys_history', $fields_values); + if (isset($this->historyRecords[$table . ':' . $id])) { + $this->getRecordHistoryStore()->modifyRecord( + $table, + $id, + $this->historyRecords[$table . ':' . $id] + ); } } + /** + * @return RecordHistoryStore + */ + protected function getRecordHistoryStore(): RecordHistoryStore + { + return GeneralUtility::makeInstance( + RecordHistoryStore::class, + RecordHistoryStore::USER_BACKEND, + $this->BE_USER->user['uid'], + $this->BE_USER->user['ses_backuserid'] ?? null, + $this->BE_USER->workspace + ); + } + /** * Update Reference Index (sys_refindex) for a record * Should be called any almost any update to a record which could affect references inside the record. diff --git a/typo3/sysext/core/Classes/History/RecordHistoryStore.php b/typo3/sysext/core/Classes/History/RecordHistoryStore.php new file mode 100644 index 000000000000..933cfde032be --- /dev/null +++ b/typo3/sysext/core/Classes/History/RecordHistoryStore.php @@ -0,0 +1,183 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Core\History; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Used to save any history to a record + * + * @internal should only be used by the TYPO3 Core + */ +class RecordHistoryStore +{ + const ACTION_ADD = 1; + const ACTION_MODIFY = 2; + const ACTION_MOVE = 3; + const ACTION_DELETE = 4; + const ACTION_UNDELETE = 5; + + const USER_BACKEND = 'BE'; + const USER_FRONTEND = 'FE'; + const USER_ANONYMOUS = ''; + + /** + * @var int|null + */ + protected $userId; + protected $userType; + protected $originalUserId; + protected $tstamp; + protected $workspaceId; + + /** + * @param int|null $userId + * @param string $userType + * @param int $originalUserId + * @param int $tstamp + * @param int $workspaceId + */ + public function __construct(string $userType = self::USER_BACKEND, int $userId = null, int $originalUserId = null, int $tstamp = null, int $workspaceId = 0) + { + $this->userId = $userId; + $this->userType = $userType; + $this->originalUserId = $originalUserId; + $this->tstamp = $tstamp ?: $GLOBALS['EXEC_TIME']; + $this->workspaceId = $workspaceId; + } + + /** + * @param string $table + * @param int $uid + * @param array $payload + * @return string + */ + public function addRecord(string $table, int $uid, array $payload): string + { + $data = [ + 'actiontype' => self::ACTION_ADD, + 'usertype' => $this->userType, + 'userid' => $this->userId, + 'originaluserid' => $this->originalUserId, + 'tablename' => $table, + 'recuid' => $uid, + 'tstamp' => $this->tstamp, + 'history_data' => json_encode($payload), + 'workspace' => $this->workspaceId, + ]; + $this->getDatabaseConnection()->insert('sys_history', $data); + return $this->getDatabaseConnection()->lastInsertId('sys_history'); + } + + /** + * @param string $table + * @param int $uid + * @param array $payload + * @return string + */ + public function modifyRecord(string $table, int $uid, array $payload): string + { + $data = [ + 'actiontype' => self::ACTION_MODIFY, + 'usertype' => $this->userType, + 'userid' => $this->userId, + 'originaluserid' => $this->originalUserId, + 'tablename' => $table, + 'recuid' => $uid, + 'tstamp' => $this->tstamp, + 'history_data' => json_encode($payload), + 'workspace' => $this->workspaceId, + ]; + $this->getDatabaseConnection()->insert('sys_history', $data); + return $this->getDatabaseConnection()->lastInsertId('sys_history'); + } + + /** + * @param string $table + * @param int $uid + * @return string + */ + public function deleteRecord(string $table, int $uid): string + { + $data = [ + 'actiontype' => self::ACTION_DELETE, + 'usertype' => $this->userType, + 'userid' => $this->userId, + 'originaluserid' => $this->originalUserId, + 'tablename' => $table, + 'recuid' => $uid, + 'tstamp' => $this->tstamp, + 'workspace' => $this->workspaceId, + ]; + $this->getDatabaseConnection()->insert('sys_history', $data); + return $this->getDatabaseConnection()->lastInsertId('sys_history'); + } + + /** + * @param string $table + * @param int $uid + * @return string + */ + public function undeleteRecord(string $table, int $uid): string + { + $data = [ + 'actiontype' => self::ACTION_UNDELETE, + 'usertype' => $this->userType, + 'userid' => $this->userId, + 'originaluserid' => $this->originalUserId, + 'tablename' => $table, + 'recuid' => $uid, + 'tstamp' => $this->tstamp, + 'workspace' => $this->workspaceId, + ]; + $this->getDatabaseConnection()->insert('sys_history', $data); + return $this->getDatabaseConnection()->lastInsertId('sys_history'); + } + + /** + * @param string $table + * @param int $uid + * @param array $payload + * @return string + */ + public function moveRecord(string $table, int $uid, array $payload): string + { + $data = [ + 'actiontype' => self::ACTION_MOVE, + 'usertype' => $this->userType, + 'userid' => $this->userId, + 'originaluserid' => $this->originalUserId, + 'tablename' => $table, + 'recuid' => $uid, + 'tstamp' => $this->tstamp, + 'history_data' => json_encode($payload), + 'workspace' => $this->workspaceId, + ]; + $this->getDatabaseConnection()->insert('sys_history', $data); + return $this->getDatabaseConnection()->lastInsertId('sys_history'); + } + + /** + * @return Connection + */ + protected function getDatabaseConnection(): Connection + { + return GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable('sys_history'); + } +} diff --git a/typo3/sysext/core/Configuration/TCA/sys_history.php b/typo3/sysext/core/Configuration/TCA/sys_history.php index 32ff967e3208..d27fbeb8ceca 100644 --- a/typo3/sysext/core/Configuration/TCA/sys_history.php +++ b/typo3/sysext/core/Configuration/TCA/sys_history.php @@ -10,24 +10,12 @@ return [ 'default_sortby' => 'uid DESC', ], 'columns' => [ - 'sys_log_uid' => [ - 'label' => 'sys_log_uid', - 'config' => [ - 'type' => 'input' - ] - ], 'history_data' => [ 'label' => 'history_data', 'config' => [ 'type' => 'input' ] ], - 'fieldlist' => [ - 'label' => 'fieldlist', - 'config' => [ - 'type' => 'input' - ] - ], 'recuid' => [ 'label' => 'recuid', 'config' => [ @@ -46,12 +34,6 @@ return [ 'type' => 'input' ] ], - 'history_files' => [ - 'label' => 'history_files', - 'config' => [ - 'type' => 'input' - ] - ], 'snapshot' => [ 'label' => 'snapshot', 'config' => [ @@ -61,7 +43,7 @@ return [ ], 'types' => [ '1' => [ - 'showitem' => 'sys_log_uid, history_data, fieldlist, recuid, tablename, tstamp, history_files, snapshot' + 'showitem' => 'history_data, recuid, tablename, tstamp, snapshot' ] ] ]; diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-55298-DecoupledHistoryFunctionality.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-55298-DecoupledHistoryFunctionality.rst new file mode 100644 index 000000000000..878fedbba443 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-55298-DecoupledHistoryFunctionality.rst @@ -0,0 +1,99 @@ +.. include:: ../../Includes.txt + +====================================================== +Breaking: #55298 - Decoupled sys_history functionality +====================================================== + +See :issue:`55298` + +Description +=========== + +Tracking of record changes within the TYPO3 Backend is now handled via the database table ``sys_history`` only, +the connection towards ``sys_log`` has been removed - at the same time, the backend view for showing the history +of a database record has been updated. + + +Database-related changes +------------------------ +Changes of database records within DataHandler are now always tracked regardless of enabled logging within DataHandler. +It is not possible to disable this functionality by design (e.g. for bulk-inserts), otherwise the history of a database +record would not be complete. + +DataHandler now tracks inserts/deletes/undelete entries into ``sys_history`` as well. Previously this was only +stored within ``sys_log`` (where it is still logged, if logging is enabled). + +Instead of having sys_history database entries that are referenced into sys_log contain all necessary data, all data +is now stored within sys_history. All additional payload data is stored as JSON and not as serialized array. + +A PHP new class php:``RecordHistory`` store has been introduced to act as API layer for storing any activity (including +moving records). + +BE-log module +------------- +Referencing history entries within the BE-Log module is now done reverse (sys_log has a reference to an existing +sys_history record, and not vice-versa), speeding up the module rendering. The following related PHP classes +have been removed which were previously needed for rendering within the BE-Log backend module: + +* php:``TYPO3\CMS\Belog\Domain\Model\HistoryEntry`` +* php:``TYPO3\CMS\Belog\Domain\Repository\HistoryEntryRepository`` +* php:``TYPO3\CMS\Belog\ViewHelpers\HistoryEntryViewHelper`` + +History view +------------ +The "highlight" functionality for selecting a specific change within the history module of the TYPO3 Backend +has been removed. + +A clear separation of concerns has been introduced between php:``ElementHistoryController``, which is the entry-point +for viewing changes of a record, and php:``RecordHistory``. The latter is now the place for fetching the history +data and doing rollbacks, where the Controller class is responsible for evaluating display-related settings inside the +module, and for preparing and rendering the Fluid-based output. + +The following public PHP methods have now been removed or made protected. + +* php:``TYPO3\CMS\Backend\History\RecordHistory->maxSteps`` (see the added setMaxSteps() method) +* php:``TYPO3\CMS\Backend\History\RecordHistory->showDiff`` +* php:``TYPO3\CMS\Backend\History\RecordHistory->showSubElements`` (see the added setShowSubElements() method) +* php:``TYPO3\CMS\Backend\History\RecordHistory->showInsertDelete`` (moved into controller) +* php:``TYPO3\CMS\Backend\History\RecordHistory->element`` +* php:``TYPO3\CMS\Backend\History\RecordHistory->lastSyslogId`` +* php:``TYPO3\CMS\Backend\History\RecordHistory->returnUrl`` +* php:``TYPO3\CMS\Backend\History\RecordHistory->showMarked`` +* php:``TYPO3\CMS\Backend\History\RecordHistory->main()`` (logic moved into controller) +* php:``TYPO3\CMS\Backend\History\RecordHistory->toggleHighlight()`` +* Method parameter of php:``TYPO3\CMS\Backend\History\RecordHistory->performRollback()`` +* php:``TYPO3\CMS\Backend\History\RecordHistory->displaySettings()`` (logic moved into controller) +* php:``TYPO3\CMS\Backend\History\RecordHistory->displayHistory()`` (logic moved into controller) +* php:``TYPO3\CMS\Backend\History\RecordHistory->displayMultipleDiff()`` (logic moved into controller) +* php:``TYPO3\CMS\Backend\History\RecordHistory->renderDiff()`` (logic moved into controller) +* php:``TYPO3\CMS\Backend\History\RecordHistory->generateTitle()`` (logic moved into controller) +* php:``TYPO3\CMS\Backend\History\RecordHistory->linkPage()`` (logic moved into view) +* php:``TYPO3\CMS\Backend\History\RecordHistory->removeFilefields()`` +* php:``TYPO3\CMS\Backend\History\RecordHistory->resolveElement()`` +* php:``TYPO3\CMS\Backend\History\RecordHistory->resolveShUid()`` +* php:``TYPO3\CMS\Backend\Controller\ContentElement\ElementHistoryController->content`` +* php:``TYPO3\CMS\Backend\Controller\ContentElement\ElementHistoryController->doc`` +* php:``TYPO3\CMS\Backend\Controller\ContentElement\ElementHistoryController->main()`` + +Impact +====== + +Calling any of the PHP methods will result in a fatal PHP error. Getting or setting any of the PHP properties +will trigger a PHP warning. + +Using the affected database tables directly will produce unexpected results than before. + + +Affected Installations +====================== + +Any installation using the record history, or extensions extending sys_history. + +Migration +========= + +An upgrade wizard to separate existing history data from ``sys_log`` can be found within the Install Tool. + +The install tool also checks for existing extensions making use of the dropped and changed PHP code. + +.. index:: Database, PHP-API, PartiallyScanned diff --git a/typo3/sysext/core/ext_tables.sql b/typo3/sysext/core/ext_tables.sql index b41201aab771..3d77eb0e0f2e 100644 --- a/typo3/sysext/core/ext_tables.sql +++ b/typo3/sysext/core/ext_tables.sql @@ -530,19 +530,20 @@ CREATE TABLE sys_collection_entries ( CREATE TABLE sys_history ( uid int(11) unsigned NOT NULL auto_increment, pid int(11) unsigned DEFAULT '0' NOT NULL, - sys_log_uid int(11) DEFAULT '0' NOT NULL, - history_data mediumtext, - fieldlist text, + actiontype tinyint(3) DEFAULT '0' NOT NULL, + usertype varchar(2) DEFAULT 'BE' NOT NULL, + userid int(11) unsigned, + originaluserid int(11) unsigned, recuid int(11) DEFAULT '0' NOT NULL, tablename varchar(255) DEFAULT '' NOT NULL, tstamp int(11) DEFAULT '0' NOT NULL, - history_files mediumtext, - snapshot int(11) DEFAULT '0' NOT NULL, + history_data mediumtext, + workspace int(11) DEFAULT '0', + PRIMARY KEY (uid), KEY parent (pid), KEY recordident_1 (tablename,recuid), - KEY recordident_2 (tablename,tstamp), - KEY sys_log_uid (sys_log_uid) + KEY recordident_2 (tablename,tstamp) ) ENGINE=InnoDB; # diff --git a/typo3/sysext/install/Classes/Updates/SeparateSysHistoryFromSysLogUpdate.php b/typo3/sysext/install/Classes/Updates/SeparateSysHistoryFromSysLogUpdate.php new file mode 100644 index 000000000000..08ab4085695d --- /dev/null +++ b/typo3/sysext/install/Classes/Updates/SeparateSysHistoryFromSysLogUpdate.php @@ -0,0 +1,175 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Install\Updates; + +/* + * This file is part of the TYPO3 CMS project. + * + * It is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, either version 2 + * of the License, or any later version. + * + * For the full copyright and license information, please read the + * LICENSE.txt file that was distributed with this source code. + * + * The TYPO3 project - inspiring people to share! + */ + +use TYPO3\CMS\Core\Database\Connection; +use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\History\RecordHistoryStore; +use TYPO3\CMS\Core\Utility\GeneralUtility; + +/** + * Merge data stored in sys_log that belongs to sys_history + */ +class SeparateSysHistoryFromSysLogUpdate extends AbstractUpdate +{ + /** + * @var string + */ + protected $title = 'Migrates existing sys_log entries into sys_history'; + + /** + * Checks if an update is needed + * + * @param string $description The description for the update + * + * @return bool Whether an update is needed (true) or not (false) + */ + public function checkForUpdate(&$description) + { + if ($this->isWizardDone()) { + return false; + } + + // sys_log field has been removed, no need to do something. + if (!$this->checkIfFieldInTableExists('sys_history', 'sys_log_uid')) { + return false; + } + + $description = 'The history of changes of a record is now solely stored within sys_history. Previous data within sys_log needs to be' + . ' migrated into sys_history now.</p>'; + + // Check if there is data to migrate + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_history'); + $queryBuilder->getRestrictions()->removeAll(); + $count = $queryBuilder->count('*') + ->from('sys_history') + ->where($queryBuilder->expr()->neq('sys_log_uid', 0)) + ->execute() + ->fetchColumn(0); + + return $count > 0; + } + + /** + * Moves data from sys_log into sys_history + * where a reference is still there: sys_history.sys_log_uid > 0 + * + * @param array $databaseQueries Queries done in this update + * @param string $customMessage Custom messages + * @return bool + * @throws \Doctrine\DBAL\DBALException + */ + public function performUpdate(array &$databaseQueries, &$customMessage) + { + // update "modify" statements (= decoupling) + $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_history'); + $queryBuilder = $connection->createQueryBuilder(); + $rows = $queryBuilder + ->select('sys_history.uid AS history_uid', 'sys_history.history_data', 'sys_log.*') + ->from('sys_history') + ->leftJoin( + 'sys_history', + 'sys_log', + 'sys_log', + $queryBuilder->expr()->eq('sys_history.sys_log_uid', $queryBuilder->quoteIdentifier('sys_log.uid')) + ) + ->execute() + ->fetchAll(); + + foreach ($rows as $row) { + $logData = $row['log_data'] !== null ? unserialize($row['log_data'], ['allowed_classes' => false]) : []; + $updateData = [ + 'actiontype' => RecordHistoryStore::ACTION_MODIFY, + 'usertype' => 'BE', + 'userid' => $row['userid'], + 'sys_log_uid' => 0, + 'history_data' => json_encode($row['history_data'] !== null ? unserialize($row['history_data'], ['allowed_classes' => false]) : []), + 'originaluserid' => (empty($logData['originalUser']) ? null : $logData['originalUser']) + ]; + $connection->update( + 'sys_history', + $updateData, + ['uid' => (int)$row['history_uid']], + ['uid' => Connection::PARAM_INT] + ); + // Store information about history entry in sys_log table + $logData['history'] = $row['history_uid']; + $connection->update( + 'sys_log', + ['log_data' => serialize($logData)], + ['uid' => (int)$row['uid']], + ['uid' => Connection::PARAM_INT] + ); + } + + // Add insert/delete calls + $logQueryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log'); + $result = $logQueryBuilder + ->select('uid', 'userid', 'action', 'tstamp', 'log_data', 'tablename', 'recuid') + ->from('sys_log') + ->where( + $logQueryBuilder->expr()->eq('type', $logQueryBuilder->createNamedParameter(1, \PDO::PARAM_INT)), + $logQueryBuilder->expr()->orX( + $logQueryBuilder->expr()->eq('action', $logQueryBuilder->createNamedParameter(1, \PDO::PARAM_INT)), + $logQueryBuilder->expr()->eq('action', $logQueryBuilder->createNamedParameter(3, \PDO::PARAM_INT)) + ) + ) + ->orderBy('uid', 'DESC') + ->execute(); + + foreach ($result as $row) { + $logData = unserialize($row['log_data']); + /** @var RecordHistoryStore $store */ + $store = GeneralUtility::makeInstance( + RecordHistoryStore::class, + RecordHistoryStore::USER_BACKEND, + $row['userid'], + (empty($logData['originalUser']) ? null : $logData['originalUser']), + $row['tstamp'] + ); + switch ($row['action']) { + // Insert + case 1: + $store->addRecord($row['tablename'], $row['recuid'], $logData); + break; + case 3: + // Delete + $store->deleteRecord($row['tablename'], $row['recuid']); + break; + } + } + $this->markWizardAsDone(); + return true; + } + + /** + * Check if given field /column in a table exists + * + * @param string $table + * @param string $fieldName + * @return bool + */ + protected function checkIfFieldInTableExists($table, $fieldName) + { + $tableColumns = GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable($table) + ->getSchemaManager() + ->listTableColumns($table); + + return isset($tableColumns[$fieldName]); + } +} diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php index b9d8c44a68c7..ba0746a6be33 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/ClassNameMatcher.php @@ -364,6 +364,21 @@ return [ 'Breaking-82334-AbstractRecordList.rst', ], ], + 'TYPO3\CMS\Belog\Domain\Model\HistoryEntry' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Belog\Domain\Repository\HistoryEntryRepository' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Belog\ViewHelpers\HistoryEntryViewHelper' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], // Removed interfaces 'TYPO3\CMS\Backend\Form\DatabaseFileIconsHookInterface' => [ diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedMatcher.php index f77a256a177a..f16b3c3ae60b 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodArgumentDroppedMatcher.php @@ -105,4 +105,10 @@ return [ 'Deprecation-81218-NoWSOLArgumentInPageRepository-getRawRecord.rst', ], ], + 'TYPO3\CMS\Backend\History\RecordHistory->performRollback' => [ + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], ]; diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php index 7eadd99f0fc2..715c2906a26e 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php @@ -1085,5 +1085,96 @@ return [ 'restFiles' => [ 'Breaking-82148-DownloadSQLDumpDroppedInEM.rst', ], - ] + ], + 'TYPO3\CMS\Backend\History\RecordHistory->main' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->toggleHighlight' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 1, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->displaySettings' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->displayHistory' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->displayMultipleDiff' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 1, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->renderDiff' => [ + 'numberOfMandatoryArguments' => 2, + 'maximumNumberOfArguments' => 3, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->generateTitle' => [ + 'numberOfMandatoryArguments' => 2, + 'maximumNumberOfArguments' => 2, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->createRollbackLink' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 3, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->linkPage' => [ + 'numberOfMandatoryArguments' => 1, + 'maximumNumberOfArguments' => 4, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->removeFilefields' => [ + 'numberOfMandatoryArguments' => 2, + 'maximumNumberOfArguments' => 2, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->resolveElement' => [ + 'numberOfMandatoryArguments' => 2, + 'maximumNumberOfArguments' => 2, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->resolveShUid' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\Controller\ContentElement\ElementHistoryController->main' => [ + 'numberOfMandatoryArguments' => 0, + 'maximumNumberOfArguments' => 0, + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], ]; diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/PropertyProtectedMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/PropertyProtectedMatcher.php index 0f8487f625d5..603d0573ecd6 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/PropertyProtectedMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/PropertyProtectedMatcher.php @@ -37,4 +37,19 @@ return [ 'Deprecation-79441-ChangeVisibilityInternalCacheDatahandler.rst', ], ], + 'TYPO3\CMS\Backend\History\RecordHistory->maxSteps' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->showSubElements' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->element' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], ]; diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/PropertyPublicMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/PropertyPublicMatcher.php index 035fbeadfc18..4c2432ef5526 100644 --- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/PropertyPublicMatcher.php +++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/PropertyPublicMatcher.php @@ -144,6 +144,41 @@ return [ 'Breaking-71306-DroppedProtocolFieldFromPageTypeLinkToExternalURL.rst', ], ], + 'TYPO3\CMS\Backend\History\RecordHistory->showInsertDelete' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->showDiff' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->lastSyslogId' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->returnUrl' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\History\RecordHistory->showMarked' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\Controller\ContentElement\ElementHistoryController->content' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], + 'TYPO3\CMS\Backend\Controller\ContentElement\ElementHistoryController->doc' => [ + 'restFiles' => [ + 'Breaking-55298-DecoupledHistoryFunctionality.rst', + ], + ], // Deprecated public properties ]; diff --git a/typo3/sysext/install/ext_localconf.php b/typo3/sysext/install/ext_localconf.php index fe291e6b2fa2..8bed37aa7fe1 100644 --- a/typo3/sysext/install/ext_localconf.php +++ b/typo3/sysext/install/ext_localconf.php @@ -44,6 +44,8 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['funcExtensio = \TYPO3\CMS\Install\Updates\FuncExtractionUpdate::class; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['pagesUrltypeField'] = \TYPO3\CMS\Install\Updates\MigrateUrlTypesInPagesUpdate::class; +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update']['separateSysHistoryFromLog'] + = \TYPO3\CMS\Install\Updates\SeparateSysHistoryFromSysLogUpdate::class; $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class); $icons = [ diff --git a/typo3/sysext/workspaces/Classes/Service/HistoryService.php b/typo3/sysext/workspaces/Classes/Service/HistoryService.php index 8f642702b409..4f2ab885bb34 100644 --- a/typo3/sysext/workspaces/Classes/Service/HistoryService.php +++ b/typo3/sysext/workspaces/Classes/Service/HistoryService.php @@ -31,7 +31,7 @@ class HistoryService implements \TYPO3\CMS\Core\SingletonInterface /** * @var array */ - protected $historyObjects = []; + protected $historyEntries = []; /** * @var \TYPO3\CMS\Core\Utility\DiffUtility @@ -57,7 +57,7 @@ class HistoryService implements \TYPO3\CMS\Core\SingletonInterface { $history = []; $i = 0; - foreach ((array)$this->getHistoryObject($table, $id)->changeLog as $entry) { + foreach ((array)$this->getHistoryEntries($table, $id) as $entry) { if ($i++ > 20) { break; } @@ -84,11 +84,11 @@ class HistoryService implements \TYPO3\CMS\Core\SingletonInterface /** @var Avatar $avatar */ $avatar = GeneralUtility::makeInstance(Avatar::class); - $beUserRecord = BackendUtility::getRecord('be_users', $entry['user']); + $beUserRecord = BackendUtility::getRecord('be_users', $entry['userid']); return [ 'datetime' => htmlspecialchars(BackendUtility::datetime($entry['tstamp'])), - 'user' => htmlspecialchars($this->getUserName($entry['user'])), + 'user' => htmlspecialchars($this->getUserName($entry['userid'])), 'user_avatar' => $avatar->render($beUserRecord), 'differences' => $differences ]; @@ -142,22 +142,20 @@ class HistoryService implements \TYPO3\CMS\Core\SingletonInterface } /** - * Gets an instance of the record history service. + * Gets an instance of the record history of a record. * * @param string $table Name of the table * @param int $id Uid of the record - * @return \TYPO3\CMS\Backend\History\RecordHistory + * @return array */ - protected function getHistoryObject($table, $id) + protected function getHistoryEntries($table, $id) { - if (!isset($this->historyObjects[$table][$id])) { + if (!isset($this->historyEntries[$table][$id])) { /** @var $historyObject \TYPO3\CMS\Backend\History\RecordHistory */ $historyObject = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\History\RecordHistory::class); - $historyObject->element = $table . ':' . $id; - $historyObject->createChangeLog(); - $this->historyObjects[$table][$id] = $historyObject; + $this->historyEntries[$table][$id] = $historyObject->getHistoryDataForRecord($table, $id); } - return $this->historyObjects[$table][$id]; + return $this->historyEntries[$table][$id]; } /** -- GitLab