[CLEANUP] The correct case must be used for standard PHP types in phpdoc
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / History / RecordHistory.php
1 <?php
2 namespace TYPO3\CMS\Backend\History;
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 TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Database\ConnectionPool;
19 use TYPO3\CMS\Core\DataHandling\DataHandler;
20 use TYPO3\CMS\Core\Imaging\Icon;
21 use TYPO3\CMS\Core\Imaging\IconFactory;
22 use TYPO3\CMS\Core\Type\Bitmask\Permission;
23 use TYPO3\CMS\Core\Utility\DiffUtility;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25 use TYPO3\CMS\Fluid\View\StandaloneView;
26
27 /**
28 * Class for the record history display module show_rechis
29 */
30 class RecordHistory
31 {
32 /**
33 * Maximum number of sys_history steps to show.
34 *
35 * @var int
36 */
37 public $maxSteps = 20;
38
39 /**
40 * Display diff or not (0-no diff, 1-inline)
41 *
42 * @var int
43 */
44 public $showDiff = 1;
45
46 /**
47 * On a pages table - show sub elements as well.
48 *
49 * @var int
50 */
51 public $showSubElements = 1;
52
53 /**
54 * Show inserts and deletes as well
55 *
56 * @var int
57 */
58 public $showInsertDelete = 1;
59
60 /**
61 * Element reference, syntax [tablename]:[uid]
62 *
63 * @var string
64 */
65 public $element;
66
67 /**
68 * syslog ID which is not shown anymore
69 *
70 * @var int
71 */
72 public $lastSyslogId;
73
74 /**
75 * @var string
76 */
77 public $returnUrl;
78
79 /**
80 * @var array
81 */
82 public $changeLog = [];
83
84 /**
85 * @var bool
86 */
87 public $showMarked = false;
88
89 /**
90 * @var array
91 */
92 protected $recordCache = [];
93
94 /**
95 * @var array
96 */
97 protected $pageAccessCache = [];
98
99 /**
100 * @var string
101 */
102 protected $rollbackFields = '';
103
104 /**
105 * @var IconFactory
106 */
107 protected $iconFactory;
108
109 /**
110 * @var StandaloneView
111 */
112 protected $view;
113
114 /**
115 * Constructor for the class
116 */
117 public function __construct()
118 {
119 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
120 // GPvars:
121 $this->element = $this->getArgument('element');
122 $this->returnUrl = $this->getArgument('returnUrl');
123 $this->lastSyslogId = $this->getArgument('diff');
124 $this->rollbackFields = $this->getArgument('rollbackFields');
125 // Resolve sh_uid if set
126 $this->resolveShUid();
127
128 $this->view = $this->getFluidTemplateObject();
129 }
130
131 /**
132 * Main function for the listing of history.
133 * It detects incoming variables like element reference, history element uid etc. and renders the correct screen.
134 *
135 * @return string HTML content for the module
136 */
137 public function main()
138 {
139 // Save snapshot
140 if ($this->getArgument('highlight') && !$this->getArgument('settings')) {
141 $this->toggleHighlight($this->getArgument('highlight'));
142 }
143
144 $this->displaySettings();
145
146 if ($this->createChangeLog()) {
147 if ($this->rollbackFields) {
148 $completeDiff = $this->createMultipleDiff();
149 $this->performRollback($completeDiff);
150 }
151 if ($this->lastSyslogId) {
152 $this->view->assign('lastSyslogId', $this->lastSyslogId);
153 $completeDiff = $this->createMultipleDiff();
154 $this->displayMultipleDiff($completeDiff);
155 }
156 if ($this->element) {
157 $this->displayHistory();
158 }
159 }
160
161 return $this->view->render();
162 }
163
164 /*******************************
165 *
166 * database actions
167 *
168 *******************************/
169 /**
170 * Toggles highlight state of record
171 *
172 * @param int $uid Uid of sys_history entry
173 */
174 public function toggleHighlight($uid)
175 {
176 $uid = (int)$uid;
177 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
178 $row = $queryBuilder
179 ->select('snapshot')
180 ->from('sys_history')
181 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
182 ->execute()
183 ->fetch();
184
185 if (!empty($row)) {
186 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
187 $queryBuilder
188 ->update('sys_history')
189 ->set('snapshot', (int)!$row['snapshot'])
190 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
191 ->execute();
192 }
193 }
194
195 /**
196 * perform rollback
197 *
198 * @param array $diff Diff array to rollback
199 * @return string
200 * @access private
201 */
202 public function performRollback($diff)
203 {
204 if (!$this->rollbackFields) {
205 return '';
206 }
207 $reloadPageFrame = 0;
208 $rollbackData = explode(':', $this->rollbackFields);
209 // PROCESS INSERTS AND DELETES
210 // rewrite inserts and deletes
211 $cmdmapArray = [];
212 $data = [];
213 if ($diff['insertsDeletes']) {
214 switch (count($rollbackData)) {
215 case 1:
216 // all tables
217 $data = $diff['insertsDeletes'];
218 break;
219 case 2:
220 // one record
221 if ($diff['insertsDeletes'][$this->rollbackFields]) {
222 $data[$this->rollbackFields] = $diff['insertsDeletes'][$this->rollbackFields];
223 }
224 break;
225 case 3:
226 // one field in one record -- ignore!
227 break;
228 }
229 if (!empty($data)) {
230 foreach ($data as $key => $action) {
231 $elParts = explode(':', $key);
232 if ((int)$action === 1) {
233 // inserted records should be deleted
234 $cmdmapArray[$elParts[0]][$elParts[1]]['delete'] = 1;
235 // When the record is deleted, the contents of the record do not need to be updated
236 unset($diff['oldData'][$key]);
237 unset($diff['newData'][$key]);
238 } elseif ((int)$action === -1) {
239 // deleted records should be inserted again
240 $cmdmapArray[$elParts[0]][$elParts[1]]['undelete'] = 1;
241 }
242 }
243 }
244 }
245 // Writes the data:
246 if ($cmdmapArray) {
247 $tce = GeneralUtility::makeInstance(DataHandler::class);
248 $tce->debug = 0;
249 $tce->dontProcessTransformations = 1;
250 $tce->start([], $cmdmapArray);
251 $tce->process_cmdmap();
252 unset($tce);
253 if (isset($cmdmapArray['pages'])) {
254 $reloadPageFrame = 1;
255 }
256 }
257 if (!$diff['insertsDeletes']) {
258 // PROCESS CHANGES
259 // create an array for process_datamap
260 $diffModified = [];
261 foreach ($diff['oldData'] as $key => $value) {
262 $splitKey = explode(':', $key);
263 $diffModified[$splitKey[0]][$splitKey[1]] = $value;
264 }
265 switch (count($rollbackData)) {
266 case 1:
267 // all tables
268 $data = $diffModified;
269 break;
270 case 2:
271 // one record
272 $data[$rollbackData[0]][$rollbackData[1]] = $diffModified[$rollbackData[0]][$rollbackData[1]];
273 break;
274 case 3:
275 // one field in one record
276 $data[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]] = $diffModified[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]];
277 break;
278 }
279 // Removing fields:
280 $data = $this->removeFilefields($rollbackData[0], $data);
281 // Writes the data:
282 $tce = GeneralUtility::makeInstance(DataHandler::class);
283 $tce->debug = 0;
284 $tce->dontProcessTransformations = 1;
285 $tce->start($data, []);
286 $tce->process_datamap();
287 unset($tce);
288 if (isset($data['pages'])) {
289 $reloadPageFrame = 1;
290 }
291 }
292 // Return to normal operation
293 $this->lastSyslogId = false;
294 $this->rollbackFields = '';
295 $this->createChangeLog();
296 $this->view->assign('reloadPageFrame', $reloadPageFrame);
297 }
298
299 /*******************************
300 *
301 * Display functions
302 *
303 *******************************/
304 /**
305 * Displays settings
306 */
307 public function displaySettings()
308 {
309 // Get current selection from UC, merge data, write it back to UC
310 $currentSelection = is_array($this->getBackendUser()->uc['moduleData']['history'])
311 ? $this->getBackendUser()->uc['moduleData']['history']
312 : ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1, 'showInsertDelete' => 1];
313 $currentSelectionOverride = $this->getArgument('settings');
314 if ($currentSelectionOverride) {
315 $currentSelection = array_merge($currentSelection, $currentSelectionOverride);
316 $this->getBackendUser()->uc['moduleData']['history'] = $currentSelection;
317 $this->getBackendUser()->writeUC($this->getBackendUser()->uc);
318 }
319 // Display selector for number of history entries
320 $selector['maxSteps'] = [
321 10 => [
322 'value' => 10
323 ],
324 20 => [
325 'value' => 20
326 ],
327 50 => [
328 'value' => 50
329 ],
330 100 => [
331 'value' => 100
332 ],
333 999 => [
334 'value' => 'maxSteps_all'
335 ],
336 'marked' => [
337 'value' => 'maxSteps_marked'
338 ]
339 ];
340 $selector['showDiff'] = [
341 0 => [
342 'value' => 'showDiff_no'
343 ],
344 1 => [
345 'value' => 'showDiff_inline'
346 ]
347 ];
348 $selector['showSubElements'] = [
349 0 => [
350 'value' => 'no'
351 ],
352 1 => [
353 'value' => 'yes'
354 ]
355 ];
356 $selector['showInsertDelete'] = [
357 0 => [
358 'value' => 'no'
359 ],
360 1 => [
361 'value' => 'yes'
362 ]
363 ];
364
365 $scriptUrl = GeneralUtility::linkThisScript();
366 $languageService = $this->getLanguageService();
367
368 foreach ($selector as $key => $values) {
369 foreach ($values as $singleKey => $singleVal) {
370 $selector[$key][$singleKey]['scriptUrl'] = htmlspecialchars(GeneralUtility::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey));
371 }
372 }
373 $this->view->assign('settings', $selector);
374 $this->view->assign('currentSelection', $currentSelection);
375 $this->view->assign('TYPO3_REQUEST_URI', htmlspecialchars(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')));
376
377 // set values correctly
378 if ($currentSelection['maxSteps'] !== 'marked') {
379 $this->maxSteps = $currentSelection['maxSteps'] ? (int)$currentSelection['maxSteps'] : $this->maxSteps;
380 } else {
381 $this->showMarked = true;
382 $this->maxSteps = false;
383 }
384 $this->showDiff = (int)$currentSelection['showDiff'];
385 $this->showSubElements = (int)$currentSelection['showSubElements'];
386 $this->showInsertDelete = (int)$currentSelection['showInsertDelete'];
387
388 // Get link to page history if the element history is shown
389 $elParts = explode(':', $this->element);
390 if (!empty($this->element) && $elParts[0] !== 'pages') {
391 $this->view->assign('singleElement', 'true');
392 $pid = $this->getRecord($elParts[0], $elParts[1]);
393
394 if ($this->hasPageAccess('pages', $pid['pid'])) {
395 $this->view->assign('fullHistoryLink', $this->linkPage(htmlspecialchars($languageService->getLL('elementHistory_link')), ['element' => 'pages:' . $pid['pid']]));
396 }
397 }
398 }
399
400 /**
401 * Shows the full change log
402 *
403 * @return string HTML for list, wrapped in a table.
404 */
405 public function displayHistory()
406 {
407 if (empty($this->changeLog)) {
408 return '';
409 }
410 $languageService = $this->getLanguageService();
411 $lines = [];
412 $beUserArray = BackendUtility::getUserNames();
413
414 $i = 0;
415
416 // Traverse changeLog array:
417 foreach ($this->changeLog as $sysLogUid => $entry) {
418 // stop after maxSteps
419 if ($this->maxSteps && $i > $this->maxSteps) {
420 break;
421 }
422 // Show only marked states
423 if (!$entry['snapshot'] && $this->showMarked) {
424 continue;
425 }
426 $i++;
427 // Build up single line
428 $singleLine = [];
429
430 // Get user names
431 $userName = $entry['user'] ? $beUserArray[$entry['user']]['username'] : $languageService->getLL('externalChange');
432 // Executed by switch-user
433 if (!empty($entry['originalUser'])) {
434 $userName .= ' (' . $languageService->getLL('viaUser') . ' ' . $beUserArray[$entry['originalUser']]['username'] . ')';
435 }
436 $singleLine['backendUserName'] = htmlspecialchars($userName);
437 $singleLine['backendUserUid'] = $entry['user'];
438 // add user name
439
440 // Diff link
441 $image = $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render();
442 $singleLine['rollbackLink']= $this->linkPage($image, ['diff' => $sysLogUid]);
443 // remove first link
444 $singleLine['time'] = htmlspecialchars(BackendUtility::datetime($entry['tstamp']));
445 // add time
446 $singleLine['age'] = htmlspecialchars(BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')));
447 // add age
448
449 $singleLine['tableUid'] = $this->linkPage(
450 $this->generateTitle($entry['tablename'], $entry['recuid']),
451 ['element' => $entry['tablename'] . ':' . $entry['recuid']],
452 '',
453 htmlspecialchars($languageService->getLL('linkRecordHistory'))
454 );
455 // add record UID
456 // Show insert/delete/diff/changed field names
457 if ($entry['action']) {
458 // insert or delete of element
459 $singleLine['action'] = htmlspecialchars($languageService->getLL($entry['action']));
460 } else {
461 // Display field names instead of full diff
462 if (!$this->showDiff) {
463 // Re-write field names with labels
464 $tmpFieldList = explode(',', $entry['fieldlist']);
465 foreach ($tmpFieldList as $key => $value) {
466 $tmp = str_replace(':', '', htmlspecialchars($languageService->sL(BackendUtility::getItemLabel($entry['tablename'], $value))));
467 if ($tmp) {
468 $tmpFieldList[$key] = $tmp;
469 } else {
470 // remove fields if no label available
471 unset($tmpFieldList[$key]);
472 }
473 }
474 $singleLine['fieldNames'] = htmlspecialchars(implode(',', $tmpFieldList));
475 } else {
476 // Display diff
477 $diff = $this->renderDiff($entry, $entry['tablename']);
478 $singleLine['differences'] = $diff;
479 }
480 }
481 // Show link to mark/unmark state
482 if (!$entry['action']) {
483 if ($entry['snapshot']) {
484 $title = htmlspecialchars($languageService->getLL('unmarkState'));
485 $image = $this->iconFactory->getIcon('actions-unmarkstate', Icon::SIZE_SMALL)->render();
486 } else {
487 $title = htmlspecialchars($languageService->getLL('markState'));
488 $image = $this->iconFactory->getIcon('actions-markstate', Icon::SIZE_SMALL)->render();
489 }
490 $singleLine['markState'] = $this->linkPage($image, ['highlight' => $entry['uid']], '', $title);
491 } else {
492 $singleLine['markState'] = '';
493 }
494 // put line together
495 $lines[] = $singleLine;
496 }
497 $this->view->assign('history', $lines);
498
499 if ($this->lastSyslogId) {
500 $this->view->assign('fullViewLink', $this->linkPage(htmlspecialchars($languageService->getLL('fullView')), ['diff' => '']));
501 }
502 }
503
504 /**
505 * Displays a diff over multiple fields including rollback links
506 *
507 * @param array $diff Difference array
508 */
509 public function displayMultipleDiff($diff)
510 {
511 // Get all array keys needed
512 $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData']));
513 $arrayKeys = array_unique($arrayKeys);
514 $languageService = $this->getLanguageService();
515 if ($arrayKeys) {
516 $lines = [];
517 foreach ($arrayKeys as $key) {
518 $singleLine = [];
519 $elParts = explode(':', $key);
520 // Turn around diff because it should be a "rollback preview"
521 if ((int)$diff['insertsDeletes'][$key] === 1) {
522 // insert
523 $singleLine['insertDelete'] = 'delete';
524 } elseif ((int)$diff['insertsDeletes'][$key] === -1) {
525 $singleLine['insertDelete'] = 'insert';
526 }
527 // Build up temporary diff array
528 // turn around diff because it should be a "rollback preview"
529 if ($diff['newData'][$key]) {
530 $tmpArr['newRecord'] = $diff['oldData'][$key];
531 $tmpArr['oldRecord'] = $diff['newData'][$key];
532 $singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], $elParts[1]);
533 }
534 $elParts = explode(':', $key);
535 $singleLine['revertRecordLink'] = $this->createRollbackLink($key, htmlspecialchars($languageService->getLL('revertRecord')), 1);
536 $singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]);
537 $lines[] = $singleLine;
538 }
539 $this->view->assign('revertAllLink', $this->createRollbackLink('ALL', htmlspecialchars($languageService->getLL('revertAll')), 0));
540 $this->view->assign('multipleDiff', $lines);
541 }
542 }
543
544 /**
545 * Renders HTML table-rows with the comparison information of an sys_history entry record
546 *
547 * @param array $entry sys_history entry record.
548 * @param string $table The table name
549 * @param int $rollbackUid If set to UID of record, display rollback links
550 * @return string|null HTML table
551 * @access private
552 */
553 public function renderDiff($entry, $table, $rollbackUid = 0)
554 {
555 $lines = [];
556 if (is_array($entry['newRecord'])) {
557 /* @var DiffUtility $diffUtility */
558 $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
559 $diffUtility->stripTags = false;
560 $fieldsToDisplay = array_keys($entry['newRecord']);
561 $languageService = $this->getLanguageService();
562 foreach ($fieldsToDisplay as $fN) {
563 if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
564 // Create diff-result:
565 $diffres = $diffUtility->makeDiffDisplay(
566 BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true),
567 BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true)
568 );
569 $lines[] = [
570 'title' => ($rollbackUid ? $this->createRollbackLink(($table . ':' . $rollbackUid . ':' . $fN), htmlspecialchars($languageService->getLL('revertField')), 2) : '') . '
571 ' . htmlspecialchars($languageService->sL(BackendUtility::getItemLabel($table, $fN))),
572 'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres))
573 ];
574 }
575 }
576 }
577 if ($lines) {
578 return $lines;
579 }
580 // error fallback
581 return null;
582 }
583
584 /*******************************
585 *
586 * build up history
587 *
588 *******************************/
589 /**
590 * Creates a diff between the current version of the records and the selected version
591 *
592 * @return array Diff for many elements, 0 if no changelog is found
593 */
594 public function createMultipleDiff()
595 {
596 $insertsDeletes = [];
597 $newArr = [];
598 $differences = [];
599 if (!$this->changeLog) {
600 return 0;
601 }
602 // traverse changelog array
603 foreach ($this->changeLog as $value) {
604 $field = $value['tablename'] . ':' . $value['recuid'];
605 // inserts / deletes
606 if ($value['action']) {
607 if (!$insertsDeletes[$field]) {
608 $insertsDeletes[$field] = 0;
609 }
610 if ($value['action'] === 'insert') {
611 $insertsDeletes[$field]++;
612 } else {
613 $insertsDeletes[$field]--;
614 }
615 // unset not needed fields
616 if ($insertsDeletes[$field] === 0) {
617 unset($insertsDeletes[$field]);
618 }
619 } else {
620 // update fields
621 // first row of field
622 if (!isset($newArr[$field])) {
623 $newArr[$field] = $value['newRecord'];
624 $differences[$field] = $value['oldRecord'];
625 } else {
626 // standard
627 $differences[$field] = array_merge($differences[$field], $value['oldRecord']);
628 }
629 }
630 }
631 // remove entries where there were no changes effectively
632 foreach ($newArr as $record => $value) {
633 foreach ($value as $key => $innerVal) {
634 if ($newArr[$record][$key] == $differences[$record][$key]) {
635 unset($newArr[$record][$key]);
636 unset($differences[$record][$key]);
637 }
638 }
639 if (empty($newArr[$record]) && empty($differences[$record])) {
640 unset($newArr[$record]);
641 unset($differences[$record]);
642 }
643 }
644 return [
645 'newData' => $newArr,
646 'oldData' => $differences,
647 'insertsDeletes' => $insertsDeletes
648 ];
649 }
650
651 /**
652 * Creates change log including sub-elements, filling $this->changeLog
653 *
654 * @return int
655 */
656 public function createChangeLog()
657 {
658 $elParts = explode(':', $this->element);
659
660 if (empty($this->element)) {
661 return 0;
662 }
663
664 $changeLog = $this->getHistoryData($elParts[0], $elParts[1]);
665 // get history of tables of this page and merge it into changelog
666 if ($elParts[0] === 'pages' && $this->showSubElements && $this->hasPageAccess('pages', $elParts[1])) {
667 foreach ($GLOBALS['TCA'] as $tablename => $value) {
668 // check if there are records on the page
669 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tablename);
670 $queryBuilder->getRestrictions()->removeAll();
671
672 $rows = $queryBuilder
673 ->select('uid')
674 ->from($tablename)
675 ->where(
676 $queryBuilder->expr()->eq(
677 'pid',
678 $queryBuilder->createNamedParameter($elParts[1], \PDO::PARAM_INT)
679 )
680 )
681 ->execute();
682 if ($rows->rowCount() === 0) {
683 continue;
684 }
685 foreach ($rows as $row) {
686 // if there is history data available, merge it into changelog
687 $newChangeLog = $this->getHistoryData($tablename, $row['uid']);
688 if (is_array($newChangeLog) && !empty($newChangeLog)) {
689 foreach ($newChangeLog as $key => $newChangeLogEntry) {
690 $changeLog[$key] = $newChangeLogEntry;
691 }
692 }
693 }
694 }
695 }
696 if (!$changeLog) {
697 return 0;
698 }
699 krsort($changeLog);
700 $this->changeLog = $changeLog;
701 return 1;
702 }
703
704 /**
705 * Gets history and delete/insert data from sys_log and sys_history
706 *
707 * @param string $table DB table name
708 * @param int $uid UID of record
709 * @return array|int Array of history data of the record or 0 if no history could be fetched
710 */
711 public function getHistoryData($table, $uid)
712 {
713 if (empty($GLOBALS['TCA'][$table]) || !$this->hasTableAccess($table) || !$this->hasPageAccess($table, $uid)) {
714 // error fallback
715 return 0;
716 }
717 // If table is found in $GLOBALS['TCA']:
718 $uid = $this->resolveElement($table, $uid);
719 // Selecting the $this->maxSteps most recent states:
720 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
721 $rows = $queryBuilder
722 ->select('sys_history.*', 'sys_log.userid', 'sys_log.log_data')
723 ->from('sys_history')
724 ->from('sys_log')
725 ->where(
726 $queryBuilder->expr()->eq(
727 'sys_history.sys_log_uid',
728 $queryBuilder->quoteIdentifier('sys_log.uid')
729 ),
730 $queryBuilder->expr()->eq(
731 'sys_history.tablename',
732 $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
733 ),
734 $queryBuilder->expr()->eq(
735 'sys_history.recuid',
736 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
737 )
738 )
739 ->orderBy('sys_log.uid', 'DESC')
740 ->setMaxResults((int)$this->maxSteps)
741 ->execute()
742 ->fetchAll();
743
744 $changeLog = [];
745 if (!empty($rows)) {
746 // Traversing the result, building up changesArray / changeLog:
747 foreach ($rows as $row) {
748 // Only history until a certain syslog ID needed
749 if ($this->lastSyslogId && $row['sys_log_uid'] < $this->lastSyslogId) {
750 continue;
751 }
752 $hisDat = unserialize($row['history_data']);
753 $logData = unserialize($row['log_data']);
754 if (is_array($hisDat['newRecord']) && is_array($hisDat['oldRecord'])) {
755 // Add information about the history to the changeLog
756 $hisDat['uid'] = $row['uid'];
757 $hisDat['tstamp'] = $row['tstamp'];
758 $hisDat['user'] = $row['userid'];
759 $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']);
760 $hisDat['snapshot'] = $row['snapshot'];
761 $hisDat['fieldlist'] = $row['fieldlist'];
762 $hisDat['tablename'] = $row['tablename'];
763 $hisDat['recuid'] = $row['recuid'];
764 $changeLog[$row['sys_log_uid']] = $hisDat;
765 } else {
766 debug('ERROR: [getHistoryData]');
767 // error fallback
768 return 0;
769 }
770 }
771 }
772 // SELECT INSERTS/DELETES
773 if ($this->showInsertDelete) {
774 // Select most recent inserts and deletes // WITHOUT snapshots
775 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
776 $result = $queryBuilder
777 ->select('uid', 'userid', 'action', 'tstamp', 'log_data')
778 ->from('sys_log')
779 ->where(
780 $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
781 $queryBuilder->expr()->orX(
782 $queryBuilder->expr()->eq('action', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
783 $queryBuilder->expr()->eq('action', $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT))
784 ),
785 $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)),
786 $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
787 )
788 ->orderBy('uid', 'DESC')
789 ->setMaxResults((int)$this->maxSteps)
790 ->execute();
791
792 // If none are found, nothing more to do
793 if ($result->rowCount() === 0) {
794 return $changeLog;
795 }
796 foreach ($result as $row) {
797 if ($this->lastSyslogId && $row['uid'] < $this->lastSyslogId) {
798 continue;
799 }
800 $hisDat = [];
801 $logData = unserialize($row['log_data']);
802 switch ($row['action']) {
803 case 1:
804 // Insert
805 $hisDat['action'] = 'insert';
806 break;
807 case 3:
808 // Delete
809 $hisDat['action'] = 'delete';
810 break;
811 }
812 $hisDat['tstamp'] = $row['tstamp'];
813 $hisDat['user'] = $row['userid'];
814 $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']);
815 $hisDat['tablename'] = $table;
816 $hisDat['recuid'] = $uid;
817 $changeLog[$row['uid']] = $hisDat;
818 }
819 }
820 return $changeLog;
821 }
822
823 /*******************************
824 *
825 * Various helper functions
826 *
827 *******************************/
828 /**
829 * Generates the title and puts the record title behind
830 *
831 * @param string $table
832 * @param string $uid
833 * @return string
834 */
835 public function generateTitle($table, $uid)
836 {
837 $out = $table . ':' . $uid;
838 if ($labelField = $GLOBALS['TCA'][$table]['ctrl']['label']) {
839 $record = $this->getRecord($table, $uid);
840 $out .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')';
841 }
842 return $out;
843 }
844
845 /**
846 * Creates a link for the rollback
847 *
848 * @param string $key Parameter which is set to rollbackFields
849 * @param string $alt Optional, alternative label and title tag of image
850 * @param int $type Optional, type of rollback: 0 - ALL; 1 - element; 2 - field
851 * @return string HTML output
852 */
853 public function createRollbackLink($key, $alt = '', $type = 0)
854 {
855 return $this->linkPage('<span class="btn btn-default" style="margin-right: 5px;">' . $alt . '</span>', ['rollbackFields' => $key]);
856 }
857
858 /**
859 * Creates a link to the same page.
860 *
861 * @param string $str String to wrap in <a> tags (must be htmlspecialchars()'ed prior to calling function)
862 * @param array $inparams Array of key/value pairs to override the default values with.
863 * @param string $anchor Possible anchor value.
864 * @param string $title Possible title.
865 * @return string Link.
866 * @access private
867 */
868 public function linkPage($str, $inparams = [], $anchor = '', $title = '')
869 {
870 // Setting default values based on GET parameters:
871 $params['element'] = $this->element;
872 $params['returnUrl'] = $this->returnUrl;
873 $params['diff'] = $this->lastSyslogId;
874 // Merging overriding values:
875 $params = array_merge($params, $inparams);
876 // Make the link:
877 $link = BackendUtility::getModuleUrl('record_history', $params) . ($anchor ? '#' . $anchor : '');
878 return '<a href="' . htmlspecialchars($link) . '"' . ($title ? ' title="' . $title . '"' : '') . '>' . $str . '</a>';
879 }
880
881 /**
882 * Will traverse the field names in $dataArray and look in $GLOBALS['TCA'] if the fields are of types which cannot
883 * be handled by the sys_history (that is currently group types with internal_type set to "file")
884 *
885 * @param string $table Table name
886 * @param array $dataArray The data array
887 * @return array The modified data array
888 * @access private
889 */
890 public function removeFilefields($table, $dataArray)
891 {
892 if ($GLOBALS['TCA'][$table]) {
893 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
894 if ($config['config']['type'] === 'group' && $config['config']['internal_type'] === 'file') {
895 unset($dataArray[$field]);
896 }
897 }
898 }
899 return $dataArray;
900 }
901
902 /**
903 * Convert input element reference to workspace version if any.
904 *
905 * @param string $table Table of input element
906 * @param int $uid UID of record
907 * @return int converted UID of record
908 */
909 public function resolveElement($table, $uid)
910 {
911 if (isset($GLOBALS['TCA'][$table])) {
912 if ($workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $uid, 'uid')) {
913 $uid = $workspaceVersion['uid'];
914 }
915 }
916 return $uid;
917 }
918
919 /**
920 * Resolve sh_uid (used from log)
921 */
922 public function resolveShUid()
923 {
924 $shUid = $this->getArgument('sh_uid');
925 if (empty($shUid)) {
926 return;
927 }
928 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
929 $record = $queryBuilder
930 ->select('*')
931 ->from('sys_history')
932 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($shUid, \PDO::PARAM_INT)))
933 ->execute()
934 ->fetch();
935
936 if (empty($record)) {
937 return;
938 }
939 $this->element = $record['tablename'] . ':' . $record['recuid'];
940 $this->lastSyslogId = $record['sys_log_uid'] - 1;
941 }
942
943 /**
944 * Determines whether user has access to a page.
945 *
946 * @param string $table
947 * @param int $uid
948 * @return bool
949 */
950 protected function hasPageAccess($table, $uid)
951 {
952 $uid = (int)$uid;
953
954 if ($table === 'pages') {
955 $pageId = $uid;
956 } else {
957 $record = $this->getRecord($table, $uid);
958 $pageId = $record['pid'];
959 }
960
961 if (!isset($this->pageAccessCache[$pageId])) {
962 $isDeletedPage = false;
963 if ($this->showInsertDelete && isset($GLOBALS['TCA']['pages']['ctrl']['delete'])) {
964 $deletedField = $GLOBALS['TCA']['pages']['ctrl']['delete'];
965 $pageRecord = $this->getRecord('pages', $pageId);
966 $isDeletedPage = (bool)$pageRecord[$deletedField];
967 }
968 if ($isDeletedPage) {
969 // The page is deleted, so we fake its uid to be the one of the parent page.
970 // By doing so, the following API will use this id to traverse the rootline
971 // and check whether it is in the users' web mounts.
972 // We check however if the user has (or better had) access to the deleted page itself.
973 // Since the only way we got here is by requesting the history of the parent page
974 // we can be sure this parent page actually exists.
975 $pageRecord['uid'] = $pageRecord['pid'];
976 $this->pageAccessCache[$pageId] = $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::PAGE_SHOW);
977 } else {
978 $this->pageAccessCache[$pageId] = BackendUtility::readPageAccess(
979 $pageId,
980 $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
981 );
982 }
983 }
984
985 return $this->pageAccessCache[$pageId] !== false;
986 }
987
988 /**
989 * Determines whether user has access to a table.
990 *
991 * @param string $table
992 * @return bool
993 */
994 protected function hasTableAccess($table)
995 {
996 return $this->getBackendUser()->check('tables_select', $table);
997 }
998
999 /**
1000 * Gets a database record.
1001 *
1002 * @param string $table
1003 * @param int $uid
1004 * @return array|null
1005 */
1006 protected function getRecord($table, $uid)
1007 {
1008 if (!isset($this->recordCache[$table][$uid])) {
1009 $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
1010 }
1011 return $this->recordCache[$table][$uid];
1012 }
1013
1014 /**
1015 * Gets the current backend user.
1016 *
1017 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1018 */
1019 protected function getBackendUser()
1020 {
1021 return $GLOBALS['BE_USER'];
1022 }
1023
1024 /**
1025 * Fetches GET/POST arguments and sanitizes the values for
1026 * the expected disposal. Invalid values will be converted
1027 * to an empty string.
1028 *
1029 * @param string $name Name of the argument
1030 * @return array|string|int
1031 */
1032 protected function getArgument($name)
1033 {
1034 $value = GeneralUtility::_GP($name);
1035
1036 switch ($name) {
1037 case 'element':
1038 if ($value !== '' && !preg_match('#^[a-z0-9_.]+:[0-9]+$#i', $value)) {
1039 $value = '';
1040 }
1041 break;
1042 case 'rollbackFields':
1043 case 'revert':
1044 if ($value !== '' && !preg_match('#^[a-z0-9_.]+(:[0-9]+(:[a-z0-9_.]+)?)?$#i', $value)) {
1045 $value = '';
1046 }
1047 break;
1048 case 'returnUrl':
1049 $value = GeneralUtility::sanitizeLocalUrl($value);
1050 break;
1051 case 'diff':
1052 case 'highlight':
1053 case 'sh_uid':
1054 $value = (int)$value;
1055 break;
1056 case 'settings':
1057 if (!is_array($value)) {
1058 $value = [];
1059 }
1060 break;
1061 default:
1062 $value = '';
1063 }
1064
1065 return $value;
1066 }
1067
1068 /**
1069 * @return \TYPO3\CMS\Lang\LanguageService
1070 */
1071 protected function getLanguageService()
1072 {
1073 return $GLOBALS['LANG'];
1074 }
1075
1076 /**
1077 * returns a new standalone view, shorthand function
1078 *
1079 * @return StandaloneView
1080 */
1081 protected function getFluidTemplateObject()
1082 {
1083 /** @var StandaloneView $view */
1084 $view = GeneralUtility::makeInstance(StandaloneView::class);
1085 $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
1086 $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
1087 $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
1088
1089 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html'));
1090
1091 $view->getRequest()->setControllerExtensionName('Backend');
1092 return $view;
1093 }
1094 }