Passed
Push — master ( d63a43...7f488e )
by
unknown
17:40 queued 04:54
created

ElementHistoryController::setPagePath()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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