2 namespace TYPO3\CMS\Backend\Controller\ContentElement
;
5 * This file is part of the TYPO3 CMS project.
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
17 use Psr\Http\Message\ResponseInterface
;
18 use Psr\Http\Message\ServerRequestInterface
;
19 use TYPO3\CMS\Backend\History\RecordHistory
;
20 use TYPO3\CMS\Backend\Template\Components\ButtonBar
;
21 use TYPO3\CMS\Backend\Template\ModuleTemplate
;
22 use TYPO3\CMS\Backend\Utility\BackendUtility
;
23 use TYPO3\CMS\Core\History\RecordHistoryStore
;
24 use TYPO3\CMS\Core\Http\HtmlResponse
;
25 use TYPO3\CMS\Core\Imaging\Icon
;
26 use TYPO3\CMS\Core\Type\Bitmask\Permission
;
27 use TYPO3\CMS\Core\Utility\DiffUtility
;
28 use TYPO3\CMS\Core\Utility\GeneralUtility
;
29 use TYPO3\CMS\Fluid\View\StandaloneView
;
32 * Controller for showing the history module of TYPO3s backend
33 * @see \TYPO3\CMS\Backend\History\RecordHistory
35 class ElementHistoryController
45 protected $historyObject;
48 * Display diff or not (0-no diff, 1-inline)
52 protected $showDiff = 1;
57 protected $recordCache = [];
60 * ModuleTemplate object
64 protected $moduleTemplate;
69 public function __construct()
71 $this->moduleTemplate
= GeneralUtility
::makeInstance(ModuleTemplate
::class);
72 $this->view
= $this->initializeView();
76 * Injects the request object for the current request or subrequest
77 * As this controller goes only through the main() method, it is rather simple for now
79 * @param ServerRequestInterface $request the current request
80 * @return ResponseInterface the response with the content
82 public function mainAction(ServerRequestInterface
$request): ResponseInterface
84 $this->moduleTemplate
->getDocHeaderComponent()->setMetaInformation([]);
86 $parsedBody = $request->getParsedBody();
87 $queryParams = $request->getQueryParams();
88 $lastHistoryEntry = (int)($parsedBody['historyEntry'] ??
$queryParams['historyEntry'] ??
0);
89 $rollbackFields = $parsedBody['rollbackFields'] ??
$queryParams['rollbackFields'] ??
null;
90 $element = $parsedBody['element'] ??
$queryParams['element'] ??
null;
91 $displaySettings = $this->prepareDisplaySettings($request);
92 $this->view
->assign('currentSelection', $displaySettings);
94 $this->showDiff
= (int)$displaySettings['showDiff'];
96 // Start history object
97 $this->historyObject
= GeneralUtility
::makeInstance(RecordHistory
::class, $element, $rollbackFields);
98 $this->historyObject
->setShowSubElements((int)$displaySettings['showSubElements']);
99 $this->historyObject
->setLastHistoryEntry($lastHistoryEntry);
100 if ($displaySettings['maxSteps']) {
101 $this->historyObject
->setMaxSteps((int)$displaySettings['maxSteps']);
104 // Do the actual logic now (rollback, show a diff for certain changes,
105 // or show the full history of a page or a specific record)
106 $this->historyObject
->createChangeLog();
107 if (!empty($this->historyObject
->changeLog
)) {
108 if ($this->historyObject
->shouldPerformRollback()) {
109 $this->historyObject
->performRollback();
110 } elseif ($lastHistoryEntry) {
111 $completeDiff = $this->historyObject
->createMultipleDiff();
112 $this->displayMultipleDiff($completeDiff);
113 $this->view
->assign('showDifferences', true);
114 $this->view
->assign('fullViewUrl', $this->buildUrl(['historyEntry' => '']));
116 if ($this->historyObject
->getElementData()) {
117 $this->displayHistory($this->historyObject
->changeLog
);
121 /** @var \TYPO3\CMS\Core\Http\NormalizedParams $normalizedParams */
122 $normalizedParams = $request->getAttribute('normalizedParams');
123 $elementData = $this->historyObject
->getElementData();
125 $this->setPagePath($elementData[0], $elementData[1]);
126 // Get link to page history if the element history is shown
127 if ($elementData[0] !== 'pages') {
128 $this->view
->assign('singleElement', true);
129 $parentPage = BackendUtility
::getRecord($elementData[0], $elementData[1], '*', '', false);
130 if ($parentPage['pid'] > 0 && BackendUtility
::readPageAccess($parentPage['pid'], $this->getBackendUser()->getPagePermsClause(Permission
::PAGE_SHOW
))) {
131 $this->view
->assign('fullHistoryUrl', $this->buildUrl([
132 'element' => 'pages:' . $parentPage['pid'],
133 'historyEntry' => '',
134 'returnUrl' => $normalizedParams->getRequestUri(),
140 $this->view
->assign('TYPO3_REQUEST_URI', $normalizedParams->getRequestUrl());
142 // Setting up the buttons and markers for docheader
143 $this->getButtons($request);
144 // Build the <body> for the module
145 $this->moduleTemplate
->setContent($this->view
->render());
147 return new HtmlResponse($this->moduleTemplate
->renderContent());
151 * Creates the correct path to the current record
153 * @param string $table
156 protected function setPagePath($table, $uid)
160 if ($table === 'pages') {
163 $record = BackendUtility
::getRecord($table, $uid, '*', '', false);
164 $pageId = $record['pid'];
167 $pageAccess = BackendUtility
::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(Permission
::PAGE_SHOW
));
168 if (is_array($pageAccess)) {
169 $this->moduleTemplate
->getDocHeaderComponent()->setMetaInformation($pageAccess);
174 * Create the panel of buttons for submitting the form or otherwise perform operations.
176 * @param ServerRequestInterface $request
178 protected function getButtons(ServerRequestInterface
$request)
180 $buttonBar = $this->moduleTemplate
->getDocHeaderComponent()->getButtonBar();
182 $helpButton = $buttonBar->makeHelpButton()
183 ->setModuleName('xMOD_csh_corebe')
184 ->setFieldName('history_log');
185 $buttonBar->addButton($helpButton);
187 // Get returnUrl parameter
188 $parsedBody = $request->getParsedBody();
189 $queryParams = $request->getQueryParams();
190 $returnUrl = GeneralUtility
::sanitizeLocalUrl($parsedBody['returnUrl'] ??
$queryParams['returnUrl'] ??
'');
193 $backButton = $buttonBar->makeLinkButton()
194 ->setHref($returnUrl)
195 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
196 ->setIcon($this->moduleTemplate
->getIconFactory()->getIcon('actions-view-go-back', Icon
::SIZE_SMALL
));
197 $buttonBar->addButton($backButton, ButtonBar
::BUTTON_POSITION_LEFT
, 10);
202 * Displays settings evaluation
204 * @param ServerRequestInterface $request
206 protected function prepareDisplaySettings(ServerRequestInterface
$request)
208 // Get current selection from UC, merge data, write it back to UC
209 $currentSelection = is_array($this->getBackendUser()->uc
['moduleData']['history'])
210 ?
$this->getBackendUser()->uc
['moduleData']['history']
211 : ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1];
212 $parsedBody = $request->getParsedBody();
213 $queryParams = $request->getQueryParams();
214 $currentSelectionOverride = $parsedBody['settings'] ??
$queryParams['settings'] ??
null;
216 if (is_array($currentSelectionOverride) && !empty($currentSelectionOverride)) {
217 $currentSelection = array_merge($currentSelection, $currentSelectionOverride);
218 $this->getBackendUser()->uc
['moduleData']['history'] = $currentSelection;
219 $this->getBackendUser()->writeUC($this->getBackendUser()->uc
);
222 // Display selector for number of history entries
223 $selector['maxSteps'] = [
237 'value' => 'maxSteps_all'
240 $selector['showDiff'] = [
242 'value' => 'showDiff_no'
245 'value' => 'showDiff_inline'
248 $selector['showSubElements'] = [
257 $scriptUrl = GeneralUtility
::linkThisScript();
259 foreach ($selector as $key => $values) {
260 foreach ($values as $singleKey => $singleVal) {
261 $selector[$key][$singleKey]['scriptUrl'] = htmlspecialchars(GeneralUtility
::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey));
264 $this->view
->assign('settings', $selector);
265 return $currentSelection;
269 * Displays a diff over multiple fields including rollback links
271 * @param array $diff Difference array
273 protected function displayMultipleDiff(array $diff)
275 // Get all array keys needed
276 $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData']));
277 $arrayKeys = array_unique($arrayKeys);
278 if (!empty($arrayKeys)) {
280 foreach ($arrayKeys as $key) {
282 $elParts = explode(':', $key);
283 // Turn around diff because it should be a "rollback preview"
284 if ((int)$diff['insertsDeletes'][$key] === 1) {
286 $singleLine['insertDelete'] = 'delete';
287 } elseif ((int)$diff['insertsDeletes'][$key] === -1) {
288 $singleLine['insertDelete'] = 'insert';
290 // Build up temporary diff array
291 // turn around diff because it should be a "rollback preview"
292 if ($diff['newData'][$key]) {
294 'newRecord' => $diff['oldData'][$key],
295 'oldRecord' => $diff['newData'][$key]
297 $singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], $elParts[1]);
299 $elParts = explode(':', $key);
300 $singleLine['revertRecordUrl'] = $this->buildUrl(['rollbackFields' => $key]);
301 $singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]);
302 $lines[] = $singleLine;
304 $this->view
->assign('revertAllUrl', $this->buildUrl(['rollbackFields' => 'ALL']));
305 $this->view
->assign('multipleDiff', $lines);
310 * Shows the full change log
312 * @param array $historyEntries
314 protected function displayHistory(array $historyEntries)
316 if (empty($historyEntries)) {
319 $languageService = $this->getLanguageService();
321 $beUserArray = BackendUtility
::getUserNames();
323 // Traverse changeLog array:
324 foreach ($historyEntries as $entry) {
325 // Build up single line
329 $singleLine['backendUserUid'] = $entry['userid'];
330 $singleLine['backendUserName'] = $entry['userid'] ?
$beUserArray[$entry['userid']]['username'] : '';
331 // Executed by switch user
332 if (!empty($entry['originaluserid'])) {
333 $singleLine['originalBackendUserName'] = $beUserArray[$entry['originaluserid']]['username'];
337 $singleLine['diffUrl'] = $this->buildUrl(['historyEntry' => $entry['uid']]);
339 $singleLine['time'] = BackendUtility
::datetime($entry['tstamp']);
341 $singleLine['age'] = BackendUtility
::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears'));
343 $singleLine['title'] = $this->generateTitle($entry['tablename'], $entry['recuid']);
344 $singleLine['elementUrl'] = $this->buildUrl(['element' => $entry['tablename'] . ':' . $entry['recuid']]);
345 $singleLine['actiontype'] = $entry['actiontype'];
346 if ((int)$entry['actiontype'] === RecordHistoryStore
::ACTION_MODIFY
) {
348 if (!$this->showDiff
) {
349 // Display field names instead of full diff
350 // Re-write field names with labels
351 $tmpFieldList = array_keys($entry['newRecord']);
352 foreach ($tmpFieldList as $key => $value) {
353 $tmp = str_replace(':', '', $languageService->sL(BackendUtility
::getItemLabel($entry['tablename'], $value)));
355 $tmpFieldList[$key] = $tmp;
357 // remove fields if no label available
358 unset($tmpFieldList[$key]);
361 $singleLine['fieldNames'] = implode(',', $tmpFieldList);
364 $singleLine['differences'] = $this->renderDiff($entry, $entry['tablename']);
368 $lines[] = $singleLine;
370 $this->view
->assign('history', $lines);
374 * Renders HTML table-rows with the comparison information of an sys_history entry record
376 * @param array $entry sys_history entry record.
377 * @param string $table The table name
378 * @param int $rollbackUid If set to UID of record, display rollback links
379 * @return array array of records
381 protected function renderDiff($entry, $table, $rollbackUid = 0): array
384 if (is_array($entry['newRecord'])) {
385 /* @var DiffUtility $diffUtility */
386 $diffUtility = GeneralUtility
::makeInstance(DiffUtility
::class);
387 $diffUtility->stripTags
= false;
388 $fieldsToDisplay = array_keys($entry['newRecord']);
389 $languageService = $this->getLanguageService();
390 foreach ($fieldsToDisplay as $fN) {
391 if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
392 // Create diff-result:
393 $diffres = $diffUtility->makeDiffDisplay(
394 BackendUtility
::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true),
395 BackendUtility
::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true)
399 $rollbackUrl = $this->buildUrl(['rollbackFields' => $table . ':' . $rollbackUid . ':' . $fN]);
402 'title' => $languageService->sL(BackendUtility
::getItemLabel($table, $fN)),
403 'rollbackUrl' => $rollbackUrl,
404 'result' => str_replace('\n', PHP_EOL
, str_replace('\r\n', '\n', $diffres))
413 * Generates the URL for a link to the current page
415 * @param array $overrideParameters
418 protected function buildUrl($overrideParameters = []): string
421 // Setting default values based on GET parameters:
422 if ($this->historyObject
->getElementData()) {
423 $params['element'] = $this->historyObject
->getElementString();
425 $params['historyEntry'] = $this->historyObject
->lastHistoryEntry
;
426 // Merging overriding values:
427 $params = array_merge($params, $overrideParameters);
430 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
431 $uriBuilder = GeneralUtility
::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder
::class);
432 return (string)$uriBuilder->buildUriFromRoute('record_history', $params);
436 * Generates the title and puts the record title behind
438 * @param string $table
442 protected function generateTitle($table, $uid): string
444 $title = $table . ':' . $uid;
445 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label'])) {
446 $record = $this->getRecord($table, $uid);
447 $title .= ' (' . BackendUtility
::getRecordTitle($table, $record, true) . ')';
453 * Gets a database record (cached).
455 * @param string $table
459 protected function getRecord($table, $uid)
461 if (!isset($this->recordCache
[$table][$uid])) {
462 $this->recordCache
[$table][$uid] = BackendUtility
::getRecord($table, $uid, '*', '', false);
464 return $this->recordCache
[$table][$uid];
468 * Returns a new standalone view, shorthand function
470 * @return StandaloneView
472 protected function initializeView()
474 /** @var StandaloneView $view */
475 $view = GeneralUtility
::makeInstance(StandaloneView
::class);
476 $view->setLayoutRootPaths([GeneralUtility
::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
477 $view->setPartialRootPaths([GeneralUtility
::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
478 $view->setTemplateRootPaths([GeneralUtility
::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
480 $view->setTemplatePathAndFilename(GeneralUtility
::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html'));
482 $view->getRequest()->setControllerExtensionName('Backend');
487 * Returns LanguageService
489 * @return \TYPO3\CMS\Core\Localization\LanguageService
491 protected function getLanguageService()
493 return $GLOBALS['LANG'];
497 * Gets the current backend user.
499 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
501 protected function getBackendUser()
503 return $GLOBALS['BE_USER'];