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>&nbsp;</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) . '&nbsp;' . $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