[TASK] Drop remaining _GP and getIndpEnv in ContentElement/ElementHistoryController
[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\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;
30
31 /**
32 * Controller for showing the history module of TYPO3s backend
33 * @see \TYPO3\CMS\Backend\History\RecordHistory
34 */
35 class ElementHistoryController
36 {
37 /**
38 * @var StandaloneView
39 */
40 protected $view;
41
42 /**
43 * @var RecordHistory
44 */
45 protected $historyObject;
46
47 /**
48 * Display diff or not (0-no diff, 1-inline)
49 *
50 * @var int
51 */
52 protected $showDiff = 1;
53
54 /**
55 * @var array
56 */
57 protected $recordCache = [];
58
59 /**
60 * ModuleTemplate object
61 *
62 * @var ModuleTemplate
63 */
64 protected $moduleTemplate;
65
66 /**
67 * Constructor
68 */
69 public function __construct()
70 {
71 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
72 $this->view = $this->initializeView();
73 }
74
75 /**
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
78 *
79 * @param ServerRequestInterface $request the current request
80 * @return ResponseInterface the response with the content
81 */
82 public function mainAction(ServerRequestInterface $request): ResponseInterface
83 {
84 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation([]);
85
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);
93
94 $this->showDiff = (int)$displaySettings['showDiff'];
95
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']);
102 }
103
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' => '']));
115 }
116 if ($this->historyObject->getElementData()) {
117 $this->displayHistory($this->historyObject->changeLog);
118 }
119 }
120
121 /** @var \TYPO3\CMS\Core\Http\NormalizedParams $normalizedParams */
122 $normalizedParams = $request->getAttribute('normalizedParams');
123 $elementData = $this->historyObject->getElementData();
124 if ($elementData) {
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(),
135 ]));
136 }
137 }
138 }
139
140 $this->view->assign('TYPO3_REQUEST_URI', $normalizedParams->getRequestUrl());
141
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());
146
147 return new HtmlResponse($this->moduleTemplate->renderContent());
148 }
149
150 /**
151 * Creates the correct path to the current record
152 *
153 * @param string $table
154 * @param int $uid
155 */
156 protected function setPagePath($table, $uid)
157 {
158 $uid = (int)$uid;
159
160 if ($table === 'pages') {
161 $pageId = $uid;
162 } else {
163 $record = BackendUtility::getRecord($table, $uid, '*', '', false);
164 $pageId = $record['pid'];
165 }
166
167 $pageAccess = BackendUtility::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
168 if (is_array($pageAccess)) {
169 $this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageAccess);
170 }
171 }
172
173 /**
174 * Create the panel of buttons for submitting the form or otherwise perform operations.
175 *
176 * @param ServerRequestInterface $request
177 */
178 protected function getButtons(ServerRequestInterface $request)
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 $parsedBody = $request->getParsedBody();
189 $queryParams = $request->getQueryParams();
190 $returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
191
192 if ($returnUrl) {
193 $backButton = $buttonBar->makeLinkButton()
194 ->setHref($returnUrl)
195 ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/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);
198 }
199 }
200
201 /**
202 * Displays settings evaluation
203 *
204 * @param ServerRequestInterface $request
205 */
206 protected function prepareDisplaySettings(ServerRequestInterface $request)
207 {
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;
215
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);
220 }
221
222 // Display selector for number of history entries
223 $selector['maxSteps'] = [
224 10 => [
225 'value' => 10
226 ],
227 20 => [
228 'value' => 20
229 ],
230 50 => [
231 'value' => 50
232 ],
233 100 => [
234 'value' => 100
235 ],
236 999 => [
237 'value' => 'maxSteps_all'
238 ]
239 ];
240 $selector['showDiff'] = [
241 0 => [
242 'value' => 'showDiff_no'
243 ],
244 1 => [
245 'value' => 'showDiff_inline'
246 ]
247 ];
248 $selector['showSubElements'] = [
249 0 => [
250 'value' => 'no'
251 ],
252 1 => [
253 'value' => 'yes'
254 ]
255 ];
256
257 $scriptUrl = GeneralUtility::linkThisScript();
258
259 foreach ($selector as $key => $values) {
260 foreach ($values as $singleKey => $singleVal) {
261 $selector[$key][$singleKey]['scriptUrl'] = htmlspecialchars(GeneralUtility::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey));
262 }
263 }
264 $this->view->assign('settings', $selector);
265 return $currentSelection;
266 }
267
268 /**
269 * Displays a diff over multiple fields including rollback links
270 *
271 * @param array $diff Difference array
272 */
273 protected function displayMultipleDiff(array $diff)
274 {
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)) {
279 $lines = [];
280 foreach ($arrayKeys as $key) {
281 $singleLine = [];
282 $elParts = explode(':', $key);
283 // Turn around diff because it should be a "rollback preview"
284 if ((int)$diff['insertsDeletes'][$key] === 1) {
285 // insert
286 $singleLine['insertDelete'] = 'delete';
287 } elseif ((int)$diff['insertsDeletes'][$key] === -1) {
288 $singleLine['insertDelete'] = 'insert';
289 }
290 // Build up temporary diff array
291 // turn around diff because it should be a "rollback preview"
292 if ($diff['newData'][$key]) {
293 $tmpArr = [
294 'newRecord' => $diff['oldData'][$key],
295 'oldRecord' => $diff['newData'][$key]
296 ];
297 $singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], $elParts[1]);
298 }
299 $elParts = explode(':', $key);
300 $singleLine['revertRecordUrl'] = $this->buildUrl(['rollbackFields' => $key]);
301 $singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]);
302 $lines[] = $singleLine;
303 }
304 $this->view->assign('revertAllUrl', $this->buildUrl(['rollbackFields' => 'ALL']));
305 $this->view->assign('multipleDiff', $lines);
306 }
307 }
308
309 /**
310 * Shows the full change log
311 *
312 * @param array $historyEntries
313 */
314 protected function displayHistory(array $historyEntries)
315 {
316 if (empty($historyEntries)) {
317 return;
318 }
319 $languageService = $this->getLanguageService();
320 $lines = [];
321 $beUserArray = BackendUtility::getUserNames();
322
323 // Traverse changeLog array:
324 foreach ($historyEntries as $entry) {
325 // Build up single line
326 $singleLine = [];
327
328 // Get user names
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'];
334 }
335
336 // Diff link
337 $singleLine['diffUrl'] = $this->buildUrl(['historyEntry' => $entry['uid']]);
338 // Add time
339 $singleLine['time'] = BackendUtility::datetime($entry['tstamp']);
340 // Add age
341 $singleLine['age'] = BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears'));
342
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) {
347 // show changes
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)));
354 if ($tmp) {
355 $tmpFieldList[$key] = $tmp;
356 } else {
357 // remove fields if no label available
358 unset($tmpFieldList[$key]);
359 }
360 }
361 $singleLine['fieldNames'] = implode(',', $tmpFieldList);
362 } else {
363 // Display diff
364 $singleLine['differences'] = $this->renderDiff($entry, $entry['tablename']);
365 }
366 }
367 // put line together
368 $lines[] = $singleLine;
369 }
370 $this->view->assign('history', $lines);
371 }
372
373 /**
374 * Renders HTML table-rows with the comparison information of an sys_history entry record
375 *
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
380 */
381 protected function renderDiff($entry, $table, $rollbackUid = 0): array
382 {
383 $lines = [];
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)
396 );
397 $rollbackUrl = '';
398 if ($rollbackUid) {
399 $rollbackUrl = $this->buildUrl(['rollbackFields' => $table . ':' . $rollbackUid . ':' . $fN]);
400 }
401 $lines[] = [
402 'title' => $languageService->sL(BackendUtility::getItemLabel($table, $fN)),
403 'rollbackUrl' => $rollbackUrl,
404 'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres))
405 ];
406 }
407 }
408 }
409 return $lines;
410 }
411
412 /**
413 * Generates the URL for a link to the current page
414 *
415 * @param array $overrideParameters
416 * @return string
417 */
418 protected function buildUrl($overrideParameters = []): string
419 {
420 $params = [];
421 // Setting default values based on GET parameters:
422 if ($this->historyObject->getElementData()) {
423 $params['element'] = $this->historyObject->getElementString();
424 }
425 $params['historyEntry'] = $this->historyObject->lastHistoryEntry;
426 // Merging overriding values:
427 $params = array_merge($params, $overrideParameters);
428 // Make the link:
429
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);
433 }
434
435 /**
436 * Generates the title and puts the record title behind
437 *
438 * @param string $table
439 * @param string $uid
440 * @return string
441 */
442 protected function generateTitle($table, $uid): string
443 {
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) . ')';
448 }
449 return $title;
450 }
451
452 /**
453 * Gets a database record (cached).
454 *
455 * @param string $table
456 * @param int $uid
457 * @return array|null
458 */
459 protected function getRecord($table, $uid)
460 {
461 if (!isset($this->recordCache[$table][$uid])) {
462 $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
463 }
464 return $this->recordCache[$table][$uid];
465 }
466
467 /**
468 * Returns a new standalone view, shorthand function
469 *
470 * @return StandaloneView
471 */
472 protected function initializeView()
473 {
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')]);
479
480 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html'));
481
482 $view->getRequest()->setControllerExtensionName('Backend');
483 return $view;
484 }
485
486 /**
487 * Returns LanguageService
488 *
489 * @return \TYPO3\CMS\Core\Localization\LanguageService
490 */
491 protected function getLanguageService()
492 {
493 return $GLOBALS['LANG'];
494 }
495
496 /**
497 * Gets the current backend user.
498 *
499 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
500 */
501 protected function getBackendUser()
502 {
503 return $GLOBALS['BE_USER'];
504 }
505 }