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