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