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