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