[TASK] Remove the "@todo define visibility" in 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 script (show_rechis.php)
23 *
24 * XHTML Compliant
25 *
26 * @author Sebastian Kurfürst <sebastian@garbage-group.de>
27 */
28 /**
29 * Class for the record history display script (show_rechis.php)
30 *
31 * @author Sebastian Kurfürst <sebastian@garbage-group.de>
32 */
33 class RecordHistory {
34
35 // External, static:
36 // Maximum number of sys_history steps to show.
37 /**
38 * @var int
39 */
40 public $maxSteps = 20;
41
42 // display diff or not (0-no diff, 1-inline)
43 /**
44 * @var int
45 */
46 public $showDiff = 1;
47
48 // on a pages table - show sub elements as well.
49 /**
50 * @var int
51 */
52 public $showSubElements = 1;
53
54 // show inserts and deletes as well
55 /**
56 * @var int
57 */
58 public $showInsertDelete = 1;
59
60 // Internal, GPvars
61 // Element reference, syntax [tablename]:[uid]
62 /**
63 * @var string
64 */
65 public $element;
66
67 // syslog ID which is not shown anymore
68 /**
69 * @var int
70 */
71 public $lastSyslogId;
72
73 /**
74 * @var string
75 */
76 public $returnUrl;
77
78 // Internal
79 /**
80 * @var array
81 */
82 public $changeLog;
83
84 /**
85 * @var bool
86 */
87 public $showMarked = FALSE;
88
89 /**
90 * @var array
91 */
92 protected $recordCache = array();
93
94 /**
95 * @var array
96 */
97 protected $pageAccessCache = array();
98
99 /**
100 * Constructor for the class
101 */
102 public function __construct() {
103 // GPvars:
104 $this->element = $this->getArgument('element');
105 $this->returnUrl = $this->getArgument('returnUrl');
106 $this->lastSyslogId = $this->getArgument('diff');
107 $this->rollbackFields = $this->getArgument('rollbackFields');
108 // Resolve sh_uid if set
109 $this->resolveShUid();
110 }
111
112 /**
113 * Main function for the listing of history.
114 * It detects incoming variables like element reference, history element uid etc. and renders the correct screen.
115 *
116 * @return HTML content for the module
117 */
118 public function main() {
119 $content = '';
120 // Single-click rollback
121 if ($this->getArgument('revert') && $this->getArgument('sumUp')) {
122 $this->rollbackFields = $this->getArgument('revert');
123 $this->showInsertDelete = 0;
124 $this->showSubElements = 0;
125 $element = explode(':', $this->element);
126 $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');
127 $record = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
128 $this->lastSyslogId = $record['sys_log_uid'];
129 $this->createChangeLog();
130 $completeDiff = $this->createMultipleDiff();
131 $this->performRollback($completeDiff);
132 \TYPO3\CMS\Core\Utility\HttpUtility::redirect($this->returnUrl);
133 }
134 // Save snapshot
135 if ($this->getArgument('highlight') && !$this->getArgument('settings')) {
136 $this->toggleHighlight($this->getArgument('highlight'));
137 }
138 $content .= $this->displaySettings();
139 if ($this->createChangeLog()) {
140 if ($this->rollbackFields) {
141 $completeDiff = $this->createMultipleDiff();
142 $content .= $this->performRollback($completeDiff);
143 }
144 if ($this->lastSyslogId) {
145 $completeDiff = $this->createMultipleDiff();
146 $content .= $this->displayMultipleDiff($completeDiff);
147 }
148 if ($this->element) {
149 $content .= $this->displayHistory();
150 }
151 }
152 return $content;
153 }
154
155 /*******************************
156 *
157 * database actions
158 *
159 *******************************/
160 /**
161 * Toggles highlight state of record
162 *
163 * @param integer $uid Uid of sys_history entry
164 * @return void
165 */
166 public function toggleHighlight($uid) {
167 $uid = (int)$uid;
168 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('snapshot', 'sys_history', 'uid=' . $uid);
169 $tmp = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
170 $GLOBALS['TYPO3_DB']->exec_UPDATEquery('sys_history', 'uid=' . $uid, array('snapshot' => !$tmp['snapshot']));
171 }
172
173 /**
174 * perform rollback
175 *
176 * @param array $diff Diff array to rollback
177 * @return void
178 * @access private
179 */
180 public function performRollback($diff) {
181 if (!$this->rollbackFields) {
182 return 0;
183 }
184 $reloadPageFrame = 0;
185 $rollbackData = explode(':', $this->rollbackFields);
186 // PROCESS INSERTS AND DELETES
187 // rewrite inserts and deletes
188 $cmdmapArray = array();
189 if ($diff['insertsDeletes']) {
190 switch (count($rollbackData)) {
191 case 1:
192 // all tables
193 $data = $diff['insertsDeletes'];
194 break;
195 case 2:
196 // one record
197 if ($diff['insertsDeletes'][$this->rollbackFields]) {
198 $data[$this->rollbackFields] = $diff['insertsDeletes'][$this->rollbackFields];
199 }
200 break;
201 case 3:
202 // one field in one record -- ignore!
203 break;
204 }
205 if ($data) {
206 foreach ($data as $key => $action) {
207 $elParts = explode(':', $key);
208 if ($action == 1) {
209 // inserted records should be deleted
210 $cmdmapArray[$elParts[0]][$elParts[1]]['delete'] = 1;
211 // When the record is deleted, the contents of the record do not need to be updated
212 unset($diff['oldData'][$key]);
213 unset($diff['newData'][$key]);
214 } elseif ($action == -1) {
215 // deleted records should be inserted again
216 $cmdmapArray[$elParts[0]][$elParts[1]]['undelete'] = 1;
217 }
218 }
219 }
220 }
221 // Writes the data:
222 if ($cmdmapArray) {
223 $tce = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler');
224 $tce->stripslashes_values = 0;
225 $tce->debug = 0;
226 $tce->dontProcessTransformations = 1;
227 $tce->start(array(), $cmdmapArray);
228 $tce->process_cmdmap();
229 unset($tce);
230 if (isset($cmdmapArray['pages'])) {
231 $reloadPageFrame = 1;
232 }
233 }
234 // PROCESS CHANGES
235 // create an array for process_datamap
236 $diff_modified = array();
237 foreach ($diff['oldData'] as $key => $value) {
238 $splitKey = explode(':', $key);
239 $diff_modified[$splitKey[0]][$splitKey[1]] = $value;
240 }
241 switch (count($rollbackData)) {
242 case 1:
243 // all tables
244 $data = $diff_modified;
245 break;
246 case 2:
247 // one record
248 $data[$rollbackData[0]][$rollbackData[1]] = $diff_modified[$rollbackData[0]][$rollbackData[1]];
249 break;
250 case 3:
251 // one field in one record
252 $data[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]] = $diff_modified[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]];
253 break;
254 }
255 // Removing fields:
256 $data = $this->removeFilefields($rollbackData[0], $data);
257 // Writes the data:
258 $tce = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler');
259 $tce->stripslashes_values = 0;
260 $tce->debug = 0;
261 $tce->dontProcessTransformations = 1;
262 $tce->start($data, array());
263 $tce->process_datamap();
264 unset($tce);
265 if (isset($data['pages'])) {
266 $reloadPageFrame = 1;
267 }
268 // Return to normal operation
269 $this->lastSyslogId = FALSE;
270 $this->rollbackFields = FALSE;
271 $this->createChangeLog();
272 // Reload page frame if necessary
273 if ($reloadPageFrame) {
274 return '<script type="text/javascript">
275 /*<![CDATA[*/
276 if (top.content && top.content.nav_frame && top.content.nav_frame.refresh_nav) {
277 top.content.nav_frame.refresh_nav();
278 }
279 /*]]>*/
280 </script>';
281 }
282 }
283
284 /*******************************
285 *
286 * Display functions
287 *
288 *******************************/
289 /**
290 * Displays settings
291 *
292 * @return string HTML code to modify settings
293 */
294 public function displaySettings() {
295 // Get current selection from UC, merge data, write it back to UC
296 $currentSelection = is_array($GLOBALS['BE_USER']->uc['moduleData']['history']) ? $GLOBALS['BE_USER']->uc['moduleData']['history'] : array('maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1, 'showInsertDelete' => 1);
297 $currentSelectionOverride = $this->getArgument('settings');
298 if ($currentSelectionOverride) {
299 $currentSelection = array_merge($currentSelection, $currentSelectionOverride);
300 $GLOBALS['BE_USER']->uc['moduleData']['history'] = $currentSelection;
301 $GLOBALS['BE_USER']->writeUC($GLOBALS['BE_USER']->uc);
302 }
303 // Display selector for number of history entries
304 $selector['maxSteps'] = array(
305 10 => 10,
306 20 => 20,
307 50 => 50,
308 100 => 100,
309 '' => 'maxSteps_all',
310 'marked' => 'maxSteps_marked'
311 );
312 $selector['showDiff'] = array(
313 0 => 'showDiff_no',
314 1 => 'showDiff_inline'
315 );
316 $selector['showSubElements'] = array(
317 0 => 'no',
318 1 => 'yes'
319 );
320 $selector['showInsertDelete'] = array(
321 0 => 'no',
322 1 => 'yes'
323 );
324 // render selectors
325 $displayCode = '';
326 foreach ($selector as $key => $values) {
327 $displayCode .= '<tr><td>' . $GLOBALS['LANG']->getLL($key, 1) . '</td>';
328 $displayCode .= '<td><select name="settings[' . $key . ']" onChange="document.settings.submit()" style="width:100px">';
329 foreach ($values as $singleKey => $singleVal) {
330 $caption = $GLOBALS['LANG']->getLL($singleVal, 1) ?: $singleVal;
331 $displayCode .= '<option value="' . $singleKey . '"' . ($singleKey == $currentSelection[$key] ? ' selected="selected"' : '') . '> ' . $caption . '</option>';
332 }
333 $displayCode .= '</select></td></tr>';
334 }
335 // set values correctly
336 if ($currentSelection['maxSteps'] != 'marked') {
337 $this->maxSteps = $currentSelection['maxSteps'] ? (int)$currentSelection['maxSteps'] : '';
338 } else {
339 $this->showMarked = TRUE;
340 $this->maxSteps = FALSE;
341 }
342 $this->showDiff = (int)$currentSelection['showDiff'];
343 $this->showSubElements = (int)$currentSelection['showSubElements'];
344 $this->showInsertDelete = (int)$currentSelection['showInsertDelete'];
345 $content = '';
346 // Get link to page history if the element history is shown
347 $elParts = explode(':', $this->element);
348 if (!empty($this->element) && $elParts[0] != 'pages') {
349 $content .= '<strong>' . $GLOBALS['LANG']->getLL('elementHistory', 1) . '</strong><br />';
350 $pid = $this->getRecord($elParts[0], $elParts[1]);
351
352 if ($this->hasPageAccess('pages', $pid['pid'])) {
353 $content .= $this->linkPage($GLOBALS['LANG']->getLL('elementHistory_link', 1), array('element' => 'pages:' . $pid['pid']));
354 }
355 }
356 $content .= '<form name="settings" action="' . htmlspecialchars(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')) . '" method="post"><table>' . $displayCode . '</table></form>';
357 return $GLOBALS['SOBE']->doc->section($GLOBALS['LANG']->getLL('settings', 1), $content, FALSE, TRUE, FALSE, FALSE);
358 }
359
360 /**
361 * Shows the full change log
362 *
363 * @return string HTML for list, wrapped in a table.
364 */
365 public function displayHistory() {
366 $lines = array();
367 // Initialize:
368 $lines[] = '<thead><tr>
369 <th> </th>
370 <th>' . $GLOBALS['LANG']->getLL('time', 1) . '</th>
371 <th>' . $GLOBALS['LANG']->getLL('age', 1) . '</th>
372 <th>' . $GLOBALS['LANG']->getLL('user', 1) . '</th>
373 <th>' . $GLOBALS['LANG']->getLL('tableUid', 1) . '</th>
374 <th>' . $GLOBALS['LANG']->getLL('differences', 1) . '</th>
375 <th>&nbsp;</th>
376 </tr></thead>';
377 $be_user_array = BackendUtility::getUserNames();
378 // Traverse changelog array:
379 if (!$this->changeLog) {
380 return 0;
381 }
382 $i = 0;
383 foreach ($this->changeLog as $sysLogUid => $entry) {
384 // stop after maxSteps
385 if ($i > $this->maxSteps && $this->maxSteps) {
386 break;
387 }
388 // Show only marked states
389 if (!$entry['snapshot'] && $this->showMarked) {
390 continue;
391 }
392 $i++;
393 // Get user names
394 $userName = $entry['user'] ? $be_user_array[$entry['user']]['username'] : $GLOBALS['LANG']->getLL('externalChange', 1);
395 // Build up single line
396 $singleLine = array();
397 // Diff link
398 $image = IconUtility::getSpriteIcon('actions-view-go-forward', array('title' => $GLOBALS['LANG']->getLL('sumUpChanges', TRUE)));
399 $singleLine[] = '<span>' . $this->linkPage($image, array('diff' => $sysLogUid)) . '</span>';
400 // remove first link
401 $singleLine[] = htmlspecialchars(BackendUtility::datetime($entry['tstamp']));
402 // add time
403 $singleLine[] = htmlspecialchars(BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')));
404 // add age
405 $singleLine[] = 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="t3-table" 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', $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 integer $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 $t3lib_diff_Obj = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Utility\\DiffUtility');
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 = $t3lib_diff_Obj->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 integer
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 integer $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 integer $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('<img ' . IconUtility::skinImg('', ('gfx/revert_' . $type . '.gif'), 'width="33" height="33"') . ' alt="' . $alt . '" title="' . $alt . '" align="middle" />', 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 integer $uid UID of record
804 * @return integer 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 integer $uid
835 * @return boolean
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 boolean
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 integer $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 }