de3a8dc0b6d70e74e92a9684d81191aa613bfbaf
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Controller / ContentElement / ElementHistoryController.php
1 <?php
2 namespace TYPO3\CMS\Backend\Controller\ContentElement;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
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.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
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\Imaging\Icon;
25 use TYPO3\CMS\Core\Utility\DiffUtility;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Fluid\View\StandaloneView;
28
29 /**
30 * Controller for showing the history module of TYPO3s backend
31 * @see \TYPO3\CMS\Backend\History\RecordHistory
32 */
33 class ElementHistoryController
34 {
35 /**
36 * @var ServerRequestInterface
37 */
38 protected $request;
39
40 /**
41 * @var StandaloneView
42 */
43 protected $view;
44
45 /**
46 * @var RecordHistory
47 */
48 protected $historyObject;
49
50 /**
51 * Display diff or not (0-no diff, 1-inline)
52 *
53 * @var int
54 */
55 protected $showDiff = 1;
56
57 /**
58 * @var array
59 */
60 protected $recordCache = [];
61
62 /**
63 * ModuleTemplate object
64 *
65 * @var ModuleTemplate
66 */
67 protected $moduleTemplate;
68
69 /**
70 * Constructor
71 */
72 public function __construct()
73 {
74 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
75 $this->view = $this->initializeView();
76 }
77
78 /**
79 * Injects the request object for the current request or subrequest
80 * As this controller goes only through the main() method, it is rather simple for now
81 *
82 * @param ServerRequestInterface $request the current request
83 * @param ResponseInterface $response
84 * @return ResponseInterface the response with the content
85 */
86 public function mainAction(ServerRequestInterface $request, ResponseInterface $response)
87 {
88 $this->request = $request;
89 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation([]);
90
91 $lastHistoryEntry = (int)($request->getParsedBody()['historyEntry'] ?: $request->getQueryParams()['historyEntry']);
92 $rollbackFields = $request->getParsedBody()['rollbackFields'] ?: $request->getQueryParams()['rollbackFields'];
93 $element = $request->getParsedBody()['element'] ?: $request->getQueryParams()['element'];
94 $displaySettings = $this->prepareDisplaySettings();
95 $this->view->assign('currentSelection', $displaySettings);
96
97 $this->showDiff = (int)$displaySettings['showDiff'];
98
99 // Start history object
100 $this->historyObject = GeneralUtility::makeInstance(RecordHistory::class, $element, $rollbackFields);
101 $this->historyObject->setShowSubElements((int)$displaySettings['showSubElements']);
102 $this->historyObject->setLastHistoryEntry($lastHistoryEntry);
103 if ($displaySettings['maxSteps']) {
104 $this->historyObject->setMaxSteps((int)$displaySettings['maxSteps']);
105 }
106
107 // Do the actual logic now (rollback, show a diff for certain changes,
108 // or show the full history of a page or a specific record)
109 $this->historyObject->createChangeLog();
110 if (!empty($this->historyObject->changeLog)) {
111 if ($this->historyObject->shouldPerformRollback()) {
112 $this->historyObject->performRollback();
113 } elseif ($lastHistoryEntry) {
114 $completeDiff = $this->historyObject->createMultipleDiff();
115 $this->displayMultipleDiff($completeDiff);
116 $this->view->assign('showDifferences', true);
117 $this->view->assign('fullViewUrl', $this->buildUrl(['historyEntry' => '']));
118 }
119 if ($this->historyObject->getElementData()) {
120 $this->displayHistory($this->historyObject->changeLog);
121 }
122 }
123
124 $elementData = $this->historyObject->getElementData();
125 if ($elementData) {
126 $this->setPagePath($elementData[0], $elementData[1]);
127 // Get link to page history if the element history is shown
128 if ($elementData[0] !== 'pages') {
129 $this->view->assign('singleElement', true);
130 $parentPage = BackendUtility::getRecord($elementData[0], $elementData[1], '*', '', false);
131 if ($parentPage['pid'] > 0 && BackendUtility::readPageAccess($parentPage['pid'], $this->getBackendUser()->getPagePermsClause(1))) {
132 $this->view->assign('fullHistoryUrl', $this->buildUrl([
133 'element' => 'pages:' . $parentPage['pid'],
134 'historyEntry' => '',
135 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI')
136 ]));
137 }
138 }
139 }
140
141 $this->view->assign('TYPO3_REQUEST_URI', GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL'));
142
143 // Setting up the buttons and markers for docheader
144 $this->getButtons();
145 // Build the <body> for the module
146 $this->moduleTemplate->setContent($this->view->render());
147
148 $response->getBody()->write($this->moduleTemplate->renderContent());
149 return $response;
150 }
151
152 /**
153 * Creates the correct path to the current record
154 *
155 * @param string $table
156 * @param int $uid
157 */
158 protected function setPagePath($table, $uid)
159 {
160 $uid = (int)$uid;
161
162 if ($table === 'pages') {
163 $pageId = $uid;
164 } else {
165 $record = BackendUtility::getRecord($table, $uid, '*', '', false);
166 $pageId = $record['pid'];
167 }
168
169 $pageAccess = BackendUtility::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(1));
170 if (is_array($pageAccess)) {
171 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageAccess);
172 }
173 }
174
175 /**
176 * Create the panel of buttons for submitting the form or otherwise perform operations.
177 */
178 protected function getButtons()
179 {
180 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
181
182 $helpButton = $buttonBar->makeHelpButton()
183 ->setModuleName('xMOD_csh_corebe')
184 ->setFieldName('history_log');
185 $buttonBar->addButton($helpButton);
186
187 // Get returnUrl parameter
188 $returnUrl = GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'));
189 if ($returnUrl) {
190 $backButton = $buttonBar->makeLinkButton()
191 ->setHref($returnUrl)
192 ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
193 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
194 $buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
195 }
196 }
197
198 /**
199 * Displays settings evaluation
200 */
201 protected function prepareDisplaySettings()
202 {
203 // Get current selection from UC, merge data, write it back to UC
204 $currentSelection = is_array($this->getBackendUser()->uc['moduleData']['history'])
205 ? $this->getBackendUser()->uc['moduleData']['history']
206 : ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1];
207 $currentSelectionOverride = $this->request->getParsedBody()['settings'] ? $this->request->getParsedBody()['settings'] : $this->request->getQueryParams()['settings'];
208 if (is_array($currentSelectionOverride) && !empty($currentSelectionOverride)) {
209 $currentSelection = array_merge($currentSelection, $currentSelectionOverride);
210 $this->getBackendUser()->uc['moduleData']['history'] = $currentSelection;
211 $this->getBackendUser()->writeUC($this->getBackendUser()->uc);
212 }
213 // Display selector for number of history entries
214 $selector['maxSteps'] = [
215 10 => [
216 'value' => 10
217 ],
218 20 => [
219 'value' => 20
220 ],
221 50 => [
222 'value' => 50
223 ],
224 100 => [
225 'value' => 100
226 ],
227 999 => [
228 'value' => 'maxSteps_all'
229 ]
230 ];
231 $selector['showDiff'] = [
232 0 => [
233 'value' => 'showDiff_no'
234 ],
235 1 => [
236 'value' => 'showDiff_inline'
237 ]
238 ];
239 $selector['showSubElements'] = [
240 0 => [
241 'value' => 'no'
242 ],
243 1 => [
244 'value' => 'yes'
245 ]
246 ];
247
248 $scriptUrl = GeneralUtility::linkThisScript();
249
250 foreach ($selector as $key => $values) {
251 foreach ($values as $singleKey => $singleVal) {
252 $selector[$key][$singleKey]['scriptUrl'] = htmlspecialchars(GeneralUtility::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey));
253 }
254 }
255 $this->view->assign('settings', $selector);
256 return $currentSelection;
257 }
258
259 /**
260 * Displays a diff over multiple fields including rollback links
261 *
262 * @param array $diff Difference array
263 */
264 protected function displayMultipleDiff(array $diff)
265 {
266 // Get all array keys needed
267 $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData']));
268 $arrayKeys = array_unique($arrayKeys);
269 if (!empty($arrayKeys)) {
270 $lines = [];
271 foreach ($arrayKeys as $key) {
272 $singleLine = [];
273 $elParts = explode(':', $key);
274 // Turn around diff because it should be a "rollback preview"
275 if ((int)$diff['insertsDeletes'][$key] === 1) {
276 // insert
277 $singleLine['insertDelete'] = 'delete';
278 } elseif ((int)$diff['insertsDeletes'][$key] === -1) {
279 $singleLine['insertDelete'] = 'insert';
280 }
281 // Build up temporary diff array
282 // turn around diff because it should be a "rollback preview"
283 if ($diff['newData'][$key]) {
284 $tmpArr = [
285 'newRecord' => $diff['oldData'][$key],
286 'oldRecord' => $diff['newData'][$key]
287 ];
288 $singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], $elParts[1]);
289 }
290 $elParts = explode(':', $key);
291 $singleLine['revertRecordUrl'] = $this->buildUrl(['rollbackFields' => $key]);
292 $singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]);
293 $lines[] = $singleLine;
294 }
295 $this->view->assign('revertAllUrl', $this->buildUrl(['rollbackFields' => 'ALL']));
296 $this->view->assign('multipleDiff', $lines);
297 }
298 }
299
300 /**
301 * Shows the full change log
302 *
303 * @param array $historyEntries
304 */
305 protected function displayHistory(array $historyEntries)
306 {
307 if (empty($historyEntries)) {
308 return;
309 }
310 $languageService = $this->getLanguageService();
311 $lines = [];
312 $beUserArray = BackendUtility::getUserNames();
313
314 // Traverse changeLog array:
315 foreach ($historyEntries as $entry) {
316 // Build up single line
317 $singleLine = [];
318
319 // Get user names
320 $singleLine['backendUserUid'] = $entry['userid'];
321 $singleLine['backendUserName'] = $entry['userid'] ? $beUserArray[$entry['userid']]['username'] : '';
322 // Executed by switch user
323 if (!empty($entry['originaluserid'])) {
324 $singleLine['originalBackendUserName'] = $beUserArray[$entry['originaluserid']]['username'];
325 }
326
327 // Diff link
328 $singleLine['diffUrl'] = $this->buildUrl(['historyEntry' => $entry['uid']]);
329 // Add time
330 $singleLine['time'] = BackendUtility::datetime($entry['tstamp']);
331 // Add age
332 $singleLine['age'] = BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears'));
333
334 $singleLine['title'] = $this->generateTitle($entry['tablename'], $entry['recuid']);
335 $singleLine['elementUrl'] = $this->buildUrl(['element' => $entry['tablename'] . ':' . $entry['recuid']]);
336 $singleLine['actiontype'] = $entry['actiontype'];
337 if ((int)$entry['actiontype'] === RecordHistoryStore::ACTION_MODIFY) {
338 // show changes
339 if (!$this->showDiff) {
340 // Display field names instead of full diff
341 // Re-write field names with labels
342 $tmpFieldList = array_keys($entry['newRecord']);
343 foreach ($tmpFieldList as $key => $value) {
344 $tmp = str_replace(':', '', $languageService->sL(BackendUtility::getItemLabel($entry['tablename'], $value)));
345 if ($tmp) {
346 $tmpFieldList[$key] = $tmp;
347 } else {
348 // remove fields if no label available
349 unset($tmpFieldList[$key]);
350 }
351 }
352 $singleLine['fieldNames'] = implode(',', $tmpFieldList);
353 } else {
354 // Display diff
355 $singleLine['differences'] = $this->renderDiff($entry, $entry['tablename']);
356 }
357 }
358 // put line together
359 $lines[] = $singleLine;
360 }
361 $this->view->assign('history', $lines);
362 }
363
364 /**
365 * Renders HTML table-rows with the comparison information of an sys_history entry record
366 *
367 * @param array $entry sys_history entry record.
368 * @param string $table The table name
369 * @param int $rollbackUid If set to UID of record, display rollback links
370 * @return array array of records
371 */
372 protected function renderDiff($entry, $table, $rollbackUid = 0): array
373 {
374 $lines = [];
375 if (is_array($entry['newRecord'])) {
376 /* @var DiffUtility $diffUtility */
377 $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
378 $diffUtility->stripTags = false;
379 $fieldsToDisplay = array_keys($entry['newRecord']);
380 $languageService = $this->getLanguageService();
381 foreach ($fieldsToDisplay as $fN) {
382 if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
383 // Create diff-result:
384 $diffres = $diffUtility->makeDiffDisplay(
385 BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true),
386 BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true)
387 );
388 $rollbackUrl = '';
389 if ($rollbackUid) {
390 $rollbackUrl = $this->buildUrl(['rollbackFields' => ($table . ':' . $rollbackUid . ':' . $fN)]);
391 }
392 $lines[] = [
393 'title' => $languageService->sL(BackendUtility::getItemLabel($table, $fN)),
394 'rollbackUrl' => $rollbackUrl,
395 'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres))
396 ];
397 }
398 }
399 }
400 return $lines;
401 }
402
403 /**
404 * Generates the URL for a link to the current page
405 *
406 * @param array $overrideParameters
407 * @return string
408 */
409 protected function buildUrl($overrideParameters = []): string
410 {
411 $params = [];
412 // Setting default values based on GET parameters:
413 if ($this->historyObject->getElementData()) {
414 $params['element'] = $this->historyObject->getElementString();
415 }
416 $params['historyEntry'] = $this->historyObject->lastHistoryEntry;
417 // Merging overriding values:
418 $params = array_merge($params, $overrideParameters);
419 // Make the link:
420 return BackendUtility::getModuleUrl('record_history', $params);
421 }
422
423 /**
424 * Generates the title and puts the record title behind
425 *
426 * @param string $table
427 * @param string $uid
428 * @return string
429 */
430 protected function generateTitle($table, $uid): string
431 {
432 $title = $table . ':' . $uid;
433 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label'])) {
434 $record = $this->getRecord($table, $uid);
435 $title .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')';
436 }
437 return $title;
438 }
439
440 /**
441 * Gets a database record (cached).
442 *
443 * @param string $table
444 * @param int $uid
445 * @return array|null
446 */
447 protected function getRecord($table, $uid)
448 {
449 if (!isset($this->recordCache[$table][$uid])) {
450 $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
451 }
452 return $this->recordCache[$table][$uid];
453 }
454
455 /**
456 * Returns a new standalone view, shorthand function
457 *
458 * @return StandaloneView
459 */
460 protected function initializeView()
461 {
462 /** @var StandaloneView $view */
463 $view = GeneralUtility::makeInstance(StandaloneView::class);
464 $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
465 $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
466 $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
467
468 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html'));
469
470 $view->getRequest()->setControllerExtensionName('Backend');
471 return $view;
472 }
473
474 /**
475 * Returns LanguageService
476 *
477 * @return \TYPO3\CMS\Core\Localization\LanguageService
478 */
479 protected function getLanguageService()
480 {
481 return $GLOBALS['LANG'];
482 }
483
484 /**
485 * Gets the current backend user.
486 *
487 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
488 */
489 protected function getBackendUser()
490 {
491 return $GLOBALS['BE_USER'];
492 }
493 }