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