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