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