[TASK] Remove ext:dbal from installation steps
[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', (int)$uid))
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', (int)$uid))
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/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 $fieldsToDisplay = array_keys($entry['newRecord']);
553 $languageService = $this->getLanguageService();
554 foreach ($fieldsToDisplay as $fN) {
555 if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
556 // Create diff-result:
557 $diffres = $diffUtility->makeDiffDisplay(
558 BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true),
559 BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true)
560 );
561 $lines[] = [
562 'title' => ($rollbackUid ? $this->createRollbackLink(($table . ':' . $rollbackUid . ':' . $fN), htmlspecialchars($languageService->getLL('revertField')), 2) : '') . '
563 ' . htmlspecialchars($languageService->sL(BackendUtility::getItemLabel($table, $fN))),
564 'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres))
565 ];
566 }
567 }
568 }
569 if ($lines) {
570 return $lines;
571 }
572 // error fallback
573 return null;
574 }
575
576 /*******************************
577 *
578 * build up history
579 *
580 *******************************/
581 /**
582 * Creates a diff between the current version of the records and the selected version
583 *
584 * @return array Diff for many elements, 0 if no changelog is found
585 */
586 public function createMultipleDiff()
587 {
588 $insertsDeletes = [];
589 $newArr = [];
590 $differences = [];
591 if (!$this->changeLog) {
592 return 0;
593 }
594 // traverse changelog array
595 foreach ($this->changeLog as $value) {
596 $field = $value['tablename'] . ':' . $value['recuid'];
597 // inserts / deletes
598 if ($value['action']) {
599 if (!$insertsDeletes[$field]) {
600 $insertsDeletes[$field] = 0;
601 }
602 if ($value['action'] === 'insert') {
603 $insertsDeletes[$field]++;
604 } else {
605 $insertsDeletes[$field]--;
606 }
607 // unset not needed fields
608 if ($insertsDeletes[$field] === 0) {
609 unset($insertsDeletes[$field]);
610 }
611 } else {
612 // update fields
613 // first row of field
614 if (!isset($newArr[$field])) {
615 $newArr[$field] = $value['newRecord'];
616 $differences[$field] = $value['oldRecord'];
617 } else {
618 // standard
619 $differences[$field] = array_merge($differences[$field], $value['oldRecord']);
620 }
621 }
622 }
623 // remove entries where there were no changes effectively
624 foreach ($newArr as $record => $value) {
625 foreach ($value as $key => $innerVal) {
626 if ($newArr[$record][$key] == $differences[$record][$key]) {
627 unset($newArr[$record][$key]);
628 unset($differences[$record][$key]);
629 }
630 }
631 if (empty($newArr[$record]) && empty($differences[$record])) {
632 unset($newArr[$record]);
633 unset($differences[$record]);
634 }
635 }
636 return [
637 'newData' => $newArr,
638 'oldData' => $differences,
639 'insertsDeletes' => $insertsDeletes
640 ];
641 }
642
643 /**
644 * Creates change log including sub-elements, filling $this->changeLog
645 *
646 * @return int
647 */
648 public function createChangeLog()
649 {
650 $elParts = explode(':', $this->element);
651
652 if (empty($this->element)) {
653 return 0;
654 }
655
656 $changeLog = $this->getHistoryData($elParts[0], $elParts[1]);
657 // get history of tables of this page and merge it into changelog
658 if ($elParts[0] == 'pages' && $this->showSubElements && $this->hasPageAccess('pages', $elParts[1])) {
659 foreach ($GLOBALS['TCA'] as $tablename => $value) {
660 // check if there are records on the page
661 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tablename);
662 $queryBuilder->getRestrictions()->removeAll();
663
664 $rows = $queryBuilder
665 ->select('uid')
666 ->from($tablename)
667 ->where($queryBuilder->expr()->eq('pid', (int)$elParts[1]))
668 ->execute();
669 if ($rows->rowCount() === 0) {
670 continue;
671 }
672 foreach ($rows as $row) {
673 // if there is history data available, merge it into changelog
674 $newChangeLog = $this->getHistoryData($tablename, $row['uid']);
675 if (is_array($newChangeLog) && !empty($newChangeLog)) {
676 foreach ($newChangeLog as $key => $newChangeLogEntry) {
677 $changeLog[$key] = $newChangeLogEntry;
678 }
679 }
680 }
681 }
682 }
683 if (!$changeLog) {
684 return 0;
685 }
686 krsort($changeLog);
687 $this->changeLog = $changeLog;
688 return 1;
689 }
690
691 /**
692 * Gets history and delete/insert data from sys_log and sys_history
693 *
694 * @param string $table DB table name
695 * @param int $uid UID of record
696 * @return array|int Array of history data of the record or 0 if no history could be fetched
697 */
698 public function getHistoryData($table, $uid)
699 {
700 if (empty($GLOBALS['TCA'][$table]) || !$this->hasTableAccess($table) || !$this->hasPageAccess($table, $uid)) {
701 // error fallback
702 return 0;
703 }
704 // If table is found in $GLOBALS['TCA']:
705 $uid = $this->resolveElement($table, $uid);
706 // Selecting the $this->maxSteps most recent states:
707 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
708 $rows = $queryBuilder
709 ->select('sys_history.*', 'sys_log.userid', 'sys_log.log_data')
710 ->from('sys_history')
711 ->from('sys_log')
712 ->where(
713 $queryBuilder->expr()->eq(
714 'sys_history.sys_log_uid',
715 $queryBuilder->quoteIdentifier('sys_log.uid')
716 ),
717 $queryBuilder->expr()->eq('sys_history.tablename', $queryBuilder->createNamedParameter($table)),
718 $queryBuilder->expr()->eq('sys_history.recuid', (int)$uid)
719 )
720 ->orderBy('sys_log.uid', 'DESC')
721 ->setMaxResults((int)$this->maxSteps)
722 ->execute()
723 ->fetchAll();
724
725 $changeLog = [];
726 if (!empty($rows)) {
727 // Traversing the result, building up changesArray / changeLog:
728 foreach ($rows as $row) {
729 // Only history until a certain syslog ID needed
730 if ($this->lastSyslogId && $row['sys_log_uid'] < $this->lastSyslogId) {
731 continue;
732 }
733 $hisDat = unserialize($row['history_data']);
734 $logData = unserialize($row['log_data']);
735 if (is_array($hisDat['newRecord']) && is_array($hisDat['oldRecord'])) {
736 // Add information about the history to the changeLog
737 $hisDat['uid'] = $row['uid'];
738 $hisDat['tstamp'] = $row['tstamp'];
739 $hisDat['user'] = $row['userid'];
740 $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']);
741 $hisDat['snapshot'] = $row['snapshot'];
742 $hisDat['fieldlist'] = $row['fieldlist'];
743 $hisDat['tablename'] = $row['tablename'];
744 $hisDat['recuid'] = $row['recuid'];
745 $changeLog[$row['sys_log_uid']] = $hisDat;
746 } else {
747 debug('ERROR: [getHistoryData]');
748 // error fallback
749 return 0;
750 }
751 }
752 }
753 // SELECT INSERTS/DELETES
754 if ($this->showInsertDelete) {
755 // Select most recent inserts and deletes // WITHOUT snapshots
756 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
757 $result = $queryBuilder
758 ->select('uid', 'userid', 'action', 'tstamp', 'log_data')
759 ->from('sys_log')
760 ->where(
761 $queryBuilder->expr()->eq('type', 1),
762 $queryBuilder->expr()->orX(
763 $queryBuilder->expr()->eq('action', 1),
764 $queryBuilder->expr()->eq('action', 3)
765 ),
766 $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table)),
767 $queryBuilder->expr()->eq('recuid', (int)$uid)
768 )
769 ->orderBy('uid', 'DESC')
770 ->setMaxResults((int)$this->maxSteps)
771 ->execute();
772
773 // If none are found, nothing more to do
774 if ($result->rowCount() === 0) {
775 return $changeLog;
776 }
777 foreach ($result as $row) {
778 if ($this->lastSyslogId && $row['uid'] < $this->lastSyslogId) {
779 continue;
780 }
781 $hisDat = [];
782 $logData = unserialize($row['log_data']);
783 switch ($row['action']) {
784 case 1:
785 // Insert
786 $hisDat['action'] = 'insert';
787 break;
788 case 3:
789 // Delete
790 $hisDat['action'] = 'delete';
791 break;
792 }
793 $hisDat['tstamp'] = $row['tstamp'];
794 $hisDat['user'] = $row['userid'];
795 $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']);
796 $hisDat['tablename'] = $table;
797 $hisDat['recuid'] = $uid;
798 $changeLog[$row['uid']] = $hisDat;
799 }
800 }
801 return $changeLog;
802 }
803
804 /*******************************
805 *
806 * Various helper functions
807 *
808 *******************************/
809 /**
810 * Generates the title and puts the record title behind
811 *
812 * @param string $table
813 * @param string $uid
814 * @return string
815 */
816 public function generateTitle($table, $uid)
817 {
818 $out = $table . ':' . $uid;
819 if ($labelField = $GLOBALS['TCA'][$table]['ctrl']['label']) {
820 $record = $this->getRecord($table, $uid);
821 $out .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')';
822 }
823 return $out;
824 }
825
826 /**
827 * Creates a link for the rollback
828 *
829 * @param string $key Parameter which is set to rollbackFields
830 * @param string $alt Optional, alternative label and title tag of image
831 * @param int $type Optional, type of rollback: 0 - ALL; 1 - element; 2 - field
832 * @return string HTML output
833 */
834 public function createRollbackLink($key, $alt = '', $type = 0)
835 {
836 return $this->linkPage('<span class="btn btn-default" style="margin-right: 5px;">' . $alt . '</span>', ['rollbackFields' => $key]);
837 }
838
839 /**
840 * Creates a link to the same page.
841 *
842 * @param string $str String to wrap in <a> tags (must be htmlspecialchars()'ed prior to calling function)
843 * @param array $inparams Array of key/value pairs to override the default values with.
844 * @param string $anchor Possible anchor value.
845 * @param string $title Possible title.
846 * @return string Link.
847 * @access private
848 */
849 public function linkPage($str, $inparams = [], $anchor = '', $title = '')
850 {
851 // Setting default values based on GET parameters:
852 $params['element'] = $this->element;
853 $params['returnUrl'] = $this->returnUrl;
854 $params['diff'] = $this->lastSyslogId;
855 // Merging overriding values:
856 $params = array_merge($params, $inparams);
857 // Make the link:
858 $link = BackendUtility::getModuleUrl('record_history', $params) . ($anchor ? '#' . $anchor : '');
859 return '<a href="' . htmlspecialchars($link) . '"' . ($title ? ' title="' . $title . '"' : '') . '>' . $str . '</a>';
860 }
861
862 /**
863 * Will traverse the field names in $dataArray and look in $GLOBALS['TCA'] if the fields are of types which cannot
864 * be handled by the sys_history (that is currently group types with internal_type set to "file")
865 *
866 * @param string $table Table name
867 * @param array $dataArray The data array
868 * @return array The modified data array
869 * @access private
870 */
871 public function removeFilefields($table, $dataArray)
872 {
873 if ($GLOBALS['TCA'][$table]) {
874 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
875 if ($config['config']['type'] === 'group' && $config['config']['internal_type'] === 'file') {
876 unset($dataArray[$field]);
877 }
878 }
879 }
880 return $dataArray;
881 }
882
883 /**
884 * Convert input element reference to workspace version if any.
885 *
886 * @param string $table Table of input element
887 * @param int $uid UID of record
888 * @return int converted UID of record
889 */
890 public function resolveElement($table, $uid)
891 {
892 if (isset($GLOBALS['TCA'][$table])) {
893 if ($workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $uid, 'uid')) {
894 $uid = $workspaceVersion['uid'];
895 }
896 }
897 return $uid;
898 }
899
900 /**
901 * Resolve sh_uid (used from log)
902 *
903 * @return void
904 */
905 public function resolveShUid()
906 {
907 $shUid = $this->getArgument('sh_uid');
908 if (empty($shUid)) {
909 return;
910 }
911 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
912 $record = $queryBuilder
913 ->select('*')
914 ->from('sys_history')
915 ->where(
916 $queryBuilder->expr()->eq('uid', (int)$shUid)
917 )
918 ->execute()
919 ->fetch();
920
921 if (empty($record)) {
922 return;
923 }
924 $this->element = $record['tablename'] . ':' . $record['recuid'];
925 $this->lastSyslogId = $record['sys_log_uid'] - 1;
926 }
927
928 /**
929 * Determines whether user has access to a page.
930 *
931 * @param string $table
932 * @param int $uid
933 * @return bool
934 */
935 protected function hasPageAccess($table, $uid)
936 {
937 $uid = (int)$uid;
938
939 if ($table === 'pages') {
940 $pageId = $uid;
941 } else {
942 $record = $this->getRecord($table, $uid);
943 $pageId = $record['pid'];
944 }
945
946 if (!isset($this->pageAccessCache[$pageId])) {
947 $this->pageAccessCache[$pageId] = BackendUtility::readPageAccess(
948 $pageId, $this->getBackendUser()->getPagePermsClause(1)
949 );
950 }
951
952 return ($this->pageAccessCache[$pageId] !== false);
953 }
954
955 /**
956 * Determines whether user has access to a table.
957 *
958 * @param string $table
959 * @return bool
960 */
961 protected function hasTableAccess($table)
962 {
963 return $this->getBackendUser()->check('tables_select', $table);
964 }
965
966 /**
967 * Gets a database record.
968 *
969 * @param string $table
970 * @param int $uid
971 * @return array|NULL
972 */
973 protected function getRecord($table, $uid)
974 {
975 if (!isset($this->recordCache[$table][$uid])) {
976 $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
977 }
978 return $this->recordCache[$table][$uid];
979 }
980
981 /**
982 * Gets the current backend user.
983 *
984 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
985 */
986 protected function getBackendUser()
987 {
988 return $GLOBALS['BE_USER'];
989 }
990
991 /**
992 * Fetches GET/POST arguments and sanitizes the values for
993 * the expected disposal. Invalid values will be converted
994 * to an empty string.
995 *
996 * @param string $name Name of the argument
997 * @return array|string|int
998 */
999 protected function getArgument($name)
1000 {
1001 $value = GeneralUtility::_GP($name);
1002
1003 switch ($name) {
1004 case 'element':
1005 if ($value !== '' && !preg_match('#^[a-z0-9_.]+:[0-9]+$#i', $value)) {
1006 $value = '';
1007 }
1008 break;
1009 case 'rollbackFields':
1010 case 'revert':
1011 if ($value !== '' && !preg_match('#^[a-z0-9_.]+(:[0-9]+(:[a-z0-9_.]+)?)?$#i', $value)) {
1012 $value = '';
1013 }
1014 break;
1015 case 'returnUrl':
1016 $value = GeneralUtility::sanitizeLocalUrl($value);
1017 break;
1018 case 'diff':
1019 case 'highlight':
1020 case 'sh_uid':
1021 $value = (int)$value;
1022 break;
1023 case 'settings':
1024 if (!is_array($value)) {
1025 $value = [];
1026 }
1027 break;
1028 default:
1029 $value = '';
1030 }
1031
1032 return $value;
1033 }
1034
1035 /**
1036 * @return \TYPO3\CMS\Lang\LanguageService
1037 */
1038 protected function getLanguageService()
1039 {
1040 return $GLOBALS['LANG'];
1041 }
1042
1043 /**
1044 * returns a new standalone view, shorthand function
1045 *
1046 * @return StandaloneView
1047 */
1048 protected function getFluidTemplateObject()
1049 {
1050 /** @var StandaloneView $view */
1051 $view = GeneralUtility::makeInstance(StandaloneView::class);
1052 $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
1053 $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
1054 $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
1055
1056 $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html'));
1057
1058 $view->getRequest()->setControllerExtensionName('Backend');
1059 return $view;
1060 }
1061 }