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