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