TYPO3  7.6
RecordHistory.php
Go to the documentation of this file.
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 
25 
30 {
36  public $maxSteps = 20;
37 
43  public $showDiff = 1;
44 
50  public $showSubElements = 1;
51 
57  public $showInsertDelete = 1;
58 
64  public $element;
65 
71  public $lastSyslogId;
72 
76  public $returnUrl;
77 
81  public $changeLog = array();
82 
86  public $showMarked = false;
87 
91  protected $recordCache = array();
92 
96  protected $pageAccessCache = array();
97 
101  protected $iconFactory;
102 
106  public function __construct()
107  {
108  $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
109  // GPvars:
110  $this->element = $this->getArgument('element');
111  $this->returnUrl = $this->getArgument('returnUrl');
112  $this->lastSyslogId = $this->getArgument('diff');
113  $this->rollbackFields = $this->getArgument('rollbackFields');
114  // Resolve sh_uid if set
115  $this->resolveShUid();
116  }
117 
124  public function main()
125  {
126  $content = '';
127  // Single-click rollback
128  if ($this->getArgument('revert') && $this->getArgument('sumUp')) {
129  $this->rollbackFields = $this->getArgument('revert');
130  $this->showInsertDelete = 0;
131  $this->showSubElements = 0;
132  $element = explode(':', $this->element);
133  $record = $this->getDatabaseConnection()->exec_SELECTgetSingleRow(
134  '*',
135  'sys_history',
136  'tablename=' . $this->getDatabaseConnection()->fullQuoteStr($element[0], 'sys_history') . ' AND recuid=' . (int)$element[1],
137  '',
138  'uid DESC'
139  );
140  $this->lastSyslogId = $record['sys_log_uid'];
141  $this->createChangeLog();
142  $completeDiff = $this->createMultipleDiff();
143  $this->performRollback($completeDiff);
144  HttpUtility::redirect($this->returnUrl);
145  }
146  // Save snapshot
147  if ($this->getArgument('highlight') && !$this->getArgument('settings')) {
148  $this->toggleHighlight($this->getArgument('highlight'));
149  }
150 
151  $content .= $this->displaySettings();
152 
153  if ($this->createChangeLog()) {
154  if ($this->rollbackFields) {
155  $completeDiff = $this->createMultipleDiff();
156  $content .= $this->performRollback($completeDiff);
157  }
158  if ($this->lastSyslogId) {
159  $completeDiff = $this->createMultipleDiff();
160  $content .= $this->displayMultipleDiff($completeDiff);
161  }
162  if ($this->element) {
163  $content .= $this->displayHistory();
164  }
165  }
166  return $content;
167  }
168 
169  /*******************************
170  *
171  * database actions
172  *
173  *******************************/
180  public function toggleHighlight($uid)
181  {
182  $uid = (int)$uid;
183  $row = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('snapshot', 'sys_history', 'uid=' . $uid);
184  if (!empty($row)) {
185  $this->getDatabaseConnection()->exec_UPDATEquery('sys_history', 'uid=' . $uid, array('snapshot' => !$row['snapshot']));
186  }
187  }
188 
196  public function performRollback($diff)
197  {
198  if (!$this->rollbackFields) {
199  return '';
200  }
201  $reloadPageFrame = 0;
202  $rollbackData = explode(':', $this->rollbackFields);
203  // PROCESS INSERTS AND DELETES
204  // rewrite inserts and deletes
205  $cmdmapArray = array();
206  $data = array();
207  if ($diff['insertsDeletes']) {
208  switch (count($rollbackData)) {
209  case 1:
210  // all tables
211  $data = $diff['insertsDeletes'];
212  break;
213  case 2:
214  // one record
215  if ($diff['insertsDeletes'][$this->rollbackFields]) {
216  $data[$this->rollbackFields] = $diff['insertsDeletes'][$this->rollbackFields];
217  }
218  break;
219  case 3:
220  // one field in one record -- ignore!
221  break;
222  }
223  if (!empty($data)) {
224  foreach ($data as $key => $action) {
225  $elParts = explode(':', $key);
226  if ((int)$action === 1) {
227  // inserted records should be deleted
228  $cmdmapArray[$elParts[0]][$elParts[1]]['delete'] = 1;
229  // When the record is deleted, the contents of the record do not need to be updated
230  unset($diff['oldData'][$key]);
231  unset($diff['newData'][$key]);
232  } elseif ((int)$action === -1) {
233  // deleted records should be inserted again
234  $cmdmapArray[$elParts[0]][$elParts[1]]['undelete'] = 1;
235  }
236  }
237  }
238  }
239  // Writes the data:
240  if ($cmdmapArray) {
241  $tce = GeneralUtility::makeInstance(DataHandler::class);
242  $tce->stripslashes_values = 0;
243  $tce->debug = 0;
244  $tce->dontProcessTransformations = 1;
245  $tce->start(array(), $cmdmapArray);
246  $tce->process_cmdmap();
247  unset($tce);
248  if (isset($cmdmapArray['pages'])) {
249  $reloadPageFrame = 1;
250  }
251  }
252  // PROCESS CHANGES
253  // create an array for process_datamap
254  $diffModified = array();
255  foreach ($diff['oldData'] as $key => $value) {
256  $splitKey = explode(':', $key);
257  $diffModified[$splitKey[0]][$splitKey[1]] = $value;
258  }
259  switch (count($rollbackData)) {
260  case 1:
261  // all tables
262  $data = $diffModified;
263  break;
264  case 2:
265  // one record
266  $data[$rollbackData[0]][$rollbackData[1]] = $diffModified[$rollbackData[0]][$rollbackData[1]];
267  break;
268  case 3:
269  // one field in one record
270  $data[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]] = $diffModified[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]];
271  break;
272  }
273  // Removing fields:
274  $data = $this->removeFilefields($rollbackData[0], $data);
275  // Writes the data:
276  $tce = GeneralUtility::makeInstance(DataHandler::class);
277  $tce->stripslashes_values = 0;
278  $tce->debug = 0;
279  $tce->dontProcessTransformations = 1;
280  $tce->start($data, array());
281  $tce->process_datamap();
282  unset($tce);
283  if (isset($data['pages'])) {
284  $reloadPageFrame = 1;
285  }
286  // Return to normal operation
287  $this->lastSyslogId = false;
288  $this->rollbackFields = false;
289  $this->createChangeLog();
290  // Reload page frame if necessary
291  if ($reloadPageFrame) {
292  return '<script type="text/javascript">
293  /*<![CDATA[*/
294  if (top.content && top.content.nav_frame && top.content.nav_frame.refresh_nav) {
295  top.content.nav_frame.refresh_nav();
296  }
297  /*]]>*/
298  </script>';
299  }
300  return '';
301  }
302 
303  /*******************************
304  *
305  * Display functions
306  *
307  *******************************/
313  public function displaySettings()
314  {
315  // Get current selection from UC, merge data, write it back to UC
316  $currentSelection = is_array($this->getBackendUser()->uc['moduleData']['history'])
317  ? $this->getBackendUser()->uc['moduleData']['history']
318  : array('maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1, 'showInsertDelete' => 1);
319  $currentSelectionOverride = $this->getArgument('settings');
320  if ($currentSelectionOverride) {
321  $currentSelection = array_merge($currentSelection, $currentSelectionOverride);
322  $this->getBackendUser()->uc['moduleData']['history'] = $currentSelection;
323  $this->getBackendUser()->writeUC($this->getBackendUser()->uc);
324  }
325  // Display selector for number of history entries
326  $selector['maxSteps'] = array(
327  10 => 10,
328  20 => 20,
329  50 => 50,
330  100 => 100,
331  '' => 'maxSteps_all',
332  'marked' => 'maxSteps_marked'
333  );
334  $selector['showDiff'] = array(
335  0 => 'showDiff_no',
336  1 => 'showDiff_inline'
337  );
338  $selector['showSubElements'] = array(
339  0 => 'no',
340  1 => 'yes'
341  );
342  $selector['showInsertDelete'] = array(
343  0 => 'no',
344  1 => 'yes'
345  );
346  // render selectors
347  $displayCode = '';
348  $scriptUrl = GeneralUtility::linkThisScript();
349  $languageService = $this->getLanguageService();
350  foreach ($selector as $key => $values) {
351  $displayCode .= '<tr><td>' . $languageService->getLL($key, true) . '</td>';
352 
353  $label = ($currentSelection[$key] !== ''
354  ? ($languageService->getLL($selector[$key][$currentSelection[$key]], true) ?: $selector[$key][$currentSelection[$key]])
355  : ($languageService->getLL($selector[$key][$currentSelection[0]], true) ?: $selector[$key][$currentSelection[0]])
356  );
357 
358  $displayCode .= '<td>
359  <div class="btn-group">
360  <button class="btn btn-default dropdown-toggle" type="button" id="copymodeSelector" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
361  ' . $label . '
362  <span class="caret"></span>
363  </button>
364  <ul class="dropdown-menu" aria-labelledby="copymodeSelector">';
365 
366  foreach ($values as $singleKey => $singleVal) {
367  $caption = $languageService->getLL($singleVal, true) ?: htmlspecialchars($singleVal);
368  $displayCode .= '<li><a href="#" onclick="document.settings.method=\'POST\'; document.settings.action=' . htmlspecialchars(GeneralUtility::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey)) . '; document.settings.submit()">' . $caption . '</a></li>';
369  }
370 
371  $displayCode .= '
372  </ul>
373  </div>
374  </td></tr>
375  ';
376  }
377  // set values correctly
378  if ($currentSelection['maxSteps'] !== 'marked') {
379  $this->maxSteps = $currentSelection['maxSteps'] ? (int)$currentSelection['maxSteps'] : '';
380  } else {
381  $this->showMarked = true;
382  $this->maxSteps = false;
383  }
384  $this->showDiff = (int)$currentSelection['showDiff'];
385  $this->showSubElements = (int)$currentSelection['showSubElements'];
386  $this->showInsertDelete = (int)$currentSelection['showInsertDelete'];
387  $content = '';
388  // Get link to page history if the element history is shown
389  $elParts = explode(':', $this->element);
390  if (!empty($this->element) && $elParts[0] !== 'pages') {
391  $content .= '<strong>' . $languageService->getLL('elementHistory', true) . '</strong><br />';
392  $pid = $this->getRecord($elParts[0], $elParts[1]);
393 
394  if ($this->hasPageAccess('pages', $pid['pid'])) {
395  $content .= $this->linkPage('<span class="btn btn-default" style="margin-bottom: 5px;">' . $languageService->getLL('elementHistory_link', true) . '</span>', array('element' => 'pages:' . $pid['pid']));
396  }
397  }
398 
399  $content .= '<a name="settings_head"></a>
400  <form name="settings" action="' . htmlspecialchars(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')) . '" method="post">
401  <div class="row">
402  <div class="col-sm-12 col-md-6 col-lg-4">
403  <div class="panel panel-default">
404  <div class="panel-heading">' . $languageService->getLL('settings', true) . '</div>
405  <table class="table">
406  ' . $displayCode . '
407  </table>
408  </div>
409  </div>
410  </div>
411  </form>
412  ';
413 
414  return '<div>' . $content . '</div>';
415  }
416 
422  public function displayHistory()
423  {
424  if (empty($this->changeLog)) {
425  return '';
426  }
427  $languageService = $this->getLanguageService();
428  $lines = array();
429  // Initialize:
430  $lines[] = '<thead><tr>
431  <th>' . $languageService->getLL('rollback', true) . '</th>
432  <th>' . $languageService->getLL('time', true) . '</th>
433  <th>' . $languageService->getLL('age', true) . '</th>
434  <th>' . $languageService->getLL('user', true) . '</th>
435  <th>' . $languageService->getLL('tableUid', true) . '</th>
436  <th>' . $languageService->getLL('differences', true) . '</th>
437  <th>&nbsp;</th>
438  </tr></thead>';
439  $beUserArray = BackendUtility::getUserNames();
440 
441  $i = 0;
443  $avatar = GeneralUtility::makeInstance(Avatar::class);
444 
445  // Traverse changeLog array:
446  foreach ($this->changeLog as $sysLogUid => $entry) {
447  // stop after maxSteps
448  if ($this->maxSteps && $i > $this->maxSteps) {
449  break;
450  }
451  // Show only marked states
452  if (!$entry['snapshot'] && $this->showMarked) {
453  continue;
454  }
455  $i++;
456  // Get user names
457  $userName = $entry['user'] ? $beUserArray[$entry['user']]['username'] : $languageService->getLL('externalChange', true);
458  // Build up single line
459  $singleLine = array();
460  // Diff link
461  $image = '<span title="' . $languageService->getLL('sumUpChanges', true) . '">' . $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render() . '</span>';
462  $singleLine[] = '<span>' . $this->linkPage($image, array('diff' => $sysLogUid)) . '</span>';
463  // remove first link
464  $singleLine[] = htmlspecialchars(BackendUtility::datetime($entry['tstamp']));
465  // add time
466  $singleLine[] = htmlspecialchars(BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')));
467  // add age
468  $userEntry = is_array($beUserArray[$entry['user']]) ? $beUserArray[$entry['user']] : null;
469  $singleLine[] = $avatar->render($userEntry) . ' ' . htmlspecialchars($userName);
470  // add user name
471  $singleLine[] = $this->linkPage(
472  $this->generateTitle($entry['tablename'], $entry['recuid']),
473  array('element' => $entry['tablename'] . ':' . $entry['recuid']),
474  '',
475  $languageService->getLL('linkRecordHistory', true)
476  );
477  // add record UID
478  // Show insert/delete/diff/changed field names
479  if ($entry['action']) {
480  // insert or delete of element
481  $singleLine[] = '<strong>' . htmlspecialchars($languageService->getLL($entry['action'], true)) . '</strong>';
482  } else {
483  // Display field names instead of full diff
484  if (!$this->showDiff) {
485  // Re-write field names with labels
486  $tmpFieldList = explode(',', $entry['fieldlist']);
487  foreach ($tmpFieldList as $key => $value) {
488  $tmp = str_replace(':', '', $languageService->sl(BackendUtility::getItemLabel($entry['tablename'], $value), true));
489  if ($tmp) {
490  $tmpFieldList[$key] = $tmp;
491  } else {
492  // remove fields if no label available
493  unset($tmpFieldList[$key]);
494  }
495  }
496  $singleLine[] = htmlspecialchars(implode(',', $tmpFieldList));
497  } else {
498  // Display diff
499  $diff = $this->renderDiff($entry, $entry['tablename']);
500  $singleLine[] = $diff;
501  }
502  }
503  // Show link to mark/unmark state
504  if (!$entry['action']) {
505  if ($entry['snapshot']) {
506  $title = $languageService->getLL('unmarkState', true);
507  $image = $this->iconFactory->getIcon('actions-unmarkstate', Icon::SIZE_SMALL)->render();
508  } else {
509  $title = $languageService->getLL('markState', true);
510  $image = $this->iconFactory->getIcon('actions-markstate', Icon::SIZE_SMALL)->render();
511  }
512  $singleLine[] = $this->linkPage($image, array('highlight' => $entry['uid']), '', $title);
513  } else {
514  $singleLine[] = '';
515  }
516  // put line together
517  $lines[] = '
518  <tr>
519  <td>' . implode('</td><td>', $singleLine) . '</td>
520  </tr>';
521  }
522 
523  // @TODO: introduce Fluid Standalone view and use callout viewHelper
524  $theCode = '<div class="callout callout-info">'
525  . '<div class="media"><div class="media-left"><span class="fa-stack fa-lg callout-icon"><i class="fa fa-circle fa-stack-2x"></i><i class="fa fa-info fa-stack-1x"></i></span></div>'
526  . '<div class="media-body">'
527  . '<p>' . $languageService->getLL('differenceMsg') . '</p>'
528  . ' <div class="callout-body">'
529  . ' </div></div></div></div>';
530 
531  // Finally, put it all together:
532  $theCode .= '
533  <!--
534  History (list):
535  -->
536 
537  <table class="table table-striped table-hover table-vertical-top" id="typo3-history">
538  ' . implode('', $lines) . '
539  </table>';
540  if ($this->lastSyslogId) {
541  $theCode .= '<br />' . $this->linkPage('<span class="btn btn-default">' . $languageService->getLL('fullView', true) . '</span>', array('diff' => ''));
542  }
543 
544  $theCode .= '<br /><br />';
545 
546  // Add the whole content as a module section:
547  return '<h2>' . $languageService->getLL('changes', true) . '</h2><div>' . $theCode . '</div>';
548  }
549 
556  public function displayMultipleDiff($diff)
557  {
558  $content = '';
559  // Get all array keys needed
560  $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData']));
561  $arrayKeys = array_unique($arrayKeys);
562  $languageService = $this->getLanguageService();
563  if ($arrayKeys) {
564  foreach ($arrayKeys as $key) {
565  $record = '';
566  $elParts = explode(':', $key);
567  // Turn around diff because it should be a "rollback preview"
568  if ((int)$diff['insertsDeletes'][$key] === 1) {
569  // insert
570  $record .= '<strong>' . $languageService->getLL('delete', true) . '</strong>';
571  $record .= '<br />';
572  } elseif ((int)$diff['insertsDeletes'][$key] === -1) {
573  $record .= '<strong>' . $languageService->getLL('insert', true) . '</strong>';
574  $record .= '<br />';
575  }
576  // Build up temporary diff array
577  // turn around diff because it should be a "rollback preview"
578  if ($diff['newData'][$key]) {
579  $tmpArr['newRecord'] = $diff['oldData'][$key];
580  $tmpArr['oldRecord'] = $diff['newData'][$key];
581  $record .= $this->renderDiff($tmpArr, $elParts[0], $elParts[1]);
582  }
583  $elParts = explode(':', $key);
584  $titleLine = $this->createRollbackLink($key, $languageService->getLL('revertRecord', true), 1) . $this->generateTitle($elParts[0], $elParts[1]);
585  $record = '<div style="padding-left:10px;border-left:5px solid darkgray;border-bottom:1px dotted darkgray;padding-bottom:2px;">' . $record . '</div>';
586  // $titleLine contains HTML, no htmlspecialchars here.
587  $content .= '<h3>' . $titleLine . '</h3><div>' . $record . '</div>';
588  }
589  $content = $this->createRollbackLink(
590  'ALL',
591  $languageService->getLL('revertAll', true),
592  0
593  ) . '<div style="padding-left:10px;border-left:5px solid darkgray;border-bottom:1px dotted darkgray;padding-bottom:2px;">' . $content . '</div>';
594  } else {
595  $content = $languageService->getLL('noDifferences', true);
596  }
597  return '<h2>' . $languageService->getLL('mergedDifferences', true) . '</h2><div>' . $content . '</div>';
598  }
599 
609  public function renderDiff($entry, $table, $rollbackUid = 0)
610  {
611  $lines = array();
612  if (is_array($entry['newRecord'])) {
613  $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
614  $fieldsToDisplay = array_keys($entry['newRecord']);
615  $languageService = $this->getLanguageService();
616  foreach ($fieldsToDisplay as $fN) {
617  if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
618  // Create diff-result:
619  $diffres = $diffUtility->makeDiffDisplay(
620  BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true),
621  BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true)
622  );
623  $lines[] = '
624  <div class="diff-item">
625  <div class="diff-item-title">
626  ' . ($rollbackUid ? $this->createRollbackLink(($table . ':' . $rollbackUid . ':' . $fN), $languageService->getLL('revertField', true), 2) : '') . '
627  ' . $languageService->sl(BackendUtility::getItemLabel($table, $fN), true) . '
628  </div>
629  <div class="diff-item-result">' . str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres)) . '</div>
630  </div>';
631  }
632  }
633  }
634  if ($lines) {
635  return '<div class="diff">' . implode('', $lines) . '</div>';
636  }
637  // error fallback
638  return null;
639  }
640 
641  /*******************************
642  *
643  * build up history
644  *
645  *******************************/
651  public function createMultipleDiff()
652  {
653  $insertsDeletes = array();
654  $newArr = array();
655  $differences = array();
656  if (!$this->changeLog) {
657  return 0;
658  }
659  // traverse changelog array
660  foreach ($this->changeLog as $value) {
661  $field = $value['tablename'] . ':' . $value['recuid'];
662  // inserts / deletes
663  if ($value['action']) {
664  if (!$insertsDeletes[$field]) {
665  $insertsDeletes[$field] = 0;
666  }
667  if ($value['action'] === 'insert') {
668  $insertsDeletes[$field]++;
669  } else {
670  $insertsDeletes[$field]--;
671  }
672  // unset not needed fields
673  if ($insertsDeletes[$field] === 0) {
674  unset($insertsDeletes[$field]);
675  }
676  } else {
677  // update fields
678  // first row of field
679  if (!isset($newArr[$field])) {
680  $newArr[$field] = $value['newRecord'];
681  $differences[$field] = $value['oldRecord'];
682  } else {
683  // standard
684  $differences[$field] = array_merge($differences[$field], $value['oldRecord']);
685  }
686  }
687  }
688  // remove entries where there were no changes effectively
689  foreach ($newArr as $record => $value) {
690  foreach ($value as $key => $innerVal) {
691  if ($newArr[$record][$key] == $differences[$record][$key]) {
692  unset($newArr[$record][$key]);
693  unset($differences[$record][$key]);
694  }
695  }
696  if (empty($newArr[$record]) && empty($differences[$record])) {
697  unset($newArr[$record]);
698  unset($differences[$record]);
699  }
700  }
701  return array(
702  'newData' => $newArr,
703  'oldData' => $differences,
704  'insertsDeletes' => $insertsDeletes
705  );
706  }
707 
713  public function createChangeLog()
714  {
715  $elParts = explode(':', $this->element);
716 
717  if (empty($this->element)) {
718  return 0;
719  }
720 
721  $changeLog = $this->getHistoryData($elParts[0], $elParts[1]);
722  // get history of tables of this page and merge it into changelog
723  if ($elParts[0] == 'pages' && $this->showSubElements && $this->hasPageAccess('pages', $elParts[1])) {
724  foreach ($GLOBALS['TCA'] as $tablename => $value) {
725  // check if there are records on the page
726  $rows = $this->getDatabaseConnection()->exec_SELECTgetRows('uid', $tablename, 'pid=' . (int)$elParts[1]);
727  if (empty($rows)) {
728  continue;
729  }
730  foreach ($rows as $row) {
731  // if there is history data available, merge it into changelog
732  $newChangeLog = $this->getHistoryData($tablename, $row['uid']);
733  if (is_array($newChangeLog) && !empty($newChangeLog)) {
734  foreach ($newChangeLog as $key => $newChangeLogEntry) {
735  $changeLog[$key] = $newChangeLogEntry;
736  }
737  }
738  }
739  }
740  }
741  if (!$changeLog) {
742  return 0;
743  }
744  krsort($changeLog);
745  $this->changeLog = $changeLog;
746  return 1;
747  }
748 
756  public function getHistoryData($table, $uid)
757  {
758  if (empty($GLOBALS['TCA'][$table]) || !$this->hasTableAccess($table) || !$this->hasPageAccess($table, $uid)) {
759  // error fallback
760  return 0;
761  }
762  // If table is found in $GLOBALS['TCA']:
763  $databaseConnection = $this->getDatabaseConnection();
764  $uid = $this->resolveElement($table, $uid);
765  // Selecting the $this->maxSteps most recent states:
766  $rows = $databaseConnection->exec_SELECTgetRows('sys_history.*, sys_log.userid', 'sys_history, sys_log', 'sys_history.sys_log_uid = sys_log.uid
767  AND sys_history.tablename = ' . $databaseConnection->fullQuoteStr($table, 'sys_history') . '
768  AND sys_history.recuid = ' . (int)$uid, '', 'sys_log.uid DESC', $this->maxSteps);
769  $changeLog = array();
770  if (!empty($rows)) {
771  // Traversing the result, building up changesArray / changeLog:
772  foreach ($rows as $row) {
773  // Only history until a certain syslog ID needed
774  if ($this->lastSyslogId && $row['sys_log_uid'] < $this->lastSyslogId) {
775  continue;
776  }
777  $hisDat = unserialize($row['history_data']);
778  if (is_array($hisDat['newRecord']) && is_array($hisDat['oldRecord'])) {
779  // Add information about the history to the changeLog
780  $hisDat['uid'] = $row['uid'];
781  $hisDat['tstamp'] = $row['tstamp'];
782  $hisDat['user'] = $row['userid'];
783  $hisDat['snapshot'] = $row['snapshot'];
784  $hisDat['fieldlist'] = $row['fieldlist'];
785  $hisDat['tablename'] = $row['tablename'];
786  $hisDat['recuid'] = $row['recuid'];
787  $changeLog[$row['sys_log_uid']] = $hisDat;
788  } else {
789  debug('ERROR: [getHistoryData]');
790  // error fallback
791  return 0;
792  }
793  }
794  }
795  // SELECT INSERTS/DELETES
796  if ($this->showInsertDelete) {
797  // Select most recent inserts and deletes // WITHOUT snapshots
798  $rows = $databaseConnection->exec_SELECTgetRows('uid, userid, action, tstamp', 'sys_log', 'type = 1
799  AND (action=1 OR action=3)
800  AND tablename = ' . $databaseConnection->fullQuoteStr($table, 'sys_log') . '
801  AND recuid = ' . (int)$uid, '', 'uid DESC', $this->maxSteps);
802  // If none are found, nothing more to do
803  if (empty($rows)) {
804  return $changeLog;
805  }
806  foreach ($rows as $row) {
807  if ($this->lastSyslogId && $row['uid'] < $this->lastSyslogId) {
808  continue;
809  }
810  $hisDat = array();
811  switch ($row['action']) {
812  case 1:
813  // Insert
814  $hisDat['action'] = 'insert';
815  break;
816  case 3:
817  // Delete
818  $hisDat['action'] = 'delete';
819  break;
820  }
821  $hisDat['tstamp'] = $row['tstamp'];
822  $hisDat['user'] = $row['userid'];
823  $hisDat['tablename'] = $table;
824  $hisDat['recuid'] = $uid;
825  $changeLog[$row['uid']] = $hisDat;
826  }
827  }
828  return $changeLog;
829  }
830 
831  /*******************************
832  *
833  * Various helper functions
834  *
835  *******************************/
843  public function generateTitle($table, $uid)
844  {
845  $out = $table . ':' . $uid;
846  if ($labelField = $GLOBALS['TCA'][$table]['ctrl']['label']) {
847  $record = $this->getRecord($table, $uid);
848  $out .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')';
849  }
850  return $out;
851  }
852 
861  public function createRollbackLink($key, $alt = '', $type = 0)
862  {
863  return $this->linkPage('<span class="btn btn-default" style="margin-right: 5px;">' . $alt . '</span>', array('rollbackFields' => $key));
864  }
865 
876  public function linkPage($str, $inparams = array(), $anchor = '', $title = '')
877  {
878  // Setting default values based on GET parameters:
879  $params['element'] = $this->element;
880  $params['returnUrl'] = $this->returnUrl;
881  $params['diff'] = $this->lastSyslogId;
882  // Merging overriding values:
883  $params = array_merge($params, $inparams);
884  // Make the link:
885  $link = BackendUtility::getModuleUrl('record_history', $params) . ($anchor ? '#' . $anchor : '');
886  return '<a href="' . htmlspecialchars($link) . '"' . ($title ? ' title="' . $title . '"' : '') . '>' . $str . '</a>';
887  }
888 
898  public function removeFilefields($table, $dataArray)
899  {
900  if ($GLOBALS['TCA'][$table]) {
901  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
902  if ($config['config']['type'] === 'group' && $config['config']['internal_type'] === 'file') {
903  unset($dataArray[$field]);
904  }
905  }
906  }
907  return $dataArray;
908  }
909 
917  public function resolveElement($table, $uid)
918  {
919  if (isset($GLOBALS['TCA'][$table])) {
920  if ($workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $uid, 'uid')) {
921  $uid = $workspaceVersion['uid'];
922  }
923  }
924  return $uid;
925  }
926 
932  public function resolveShUid()
933  {
934  $shUid = $this->getArgument('sh_uid');
935  if (empty($shUid)) {
936  return;
937  }
938  $record = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('*', 'sys_history', 'uid=' . (int)$shUid);
939  if (empty($record)) {
940  return;
941  }
942  $this->element = $record['tablename'] . ':' . $record['recuid'];
943  $this->lastSyslogId = $record['sys_log_uid'] - 1;
944  }
945 
953  protected function hasPageAccess($table, $uid)
954  {
955  $uid = (int)$uid;
956 
957  if ($table === 'pages') {
958  $pageId = $uid;
959  } else {
960  $record = $this->getRecord($table, $uid);
961  $pageId = $record['pid'];
962  }
963 
964  if (!isset($this->pageAccessCache[$pageId])) {
965  $this->pageAccessCache[$pageId] = BackendUtility::readPageAccess(
966  $pageId, $this->getBackendUser()->getPagePermsClause(1)
967  );
968  }
969 
970  return ($this->pageAccessCache[$pageId] !== false);
971  }
972 
979  protected function hasTableAccess($table)
980  {
981  return $this->getBackendUser()->check('tables_select', $table);
982  }
983 
991  protected function getRecord($table, $uid)
992  {
993  if (!isset($this->recordCache[$table][$uid])) {
994  $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
995  }
996  return $this->recordCache[$table][$uid];
997  }
998 
1004  protected function getBackendUser()
1005  {
1006  return $GLOBALS['BE_USER'];
1007  }
1008 
1017  protected function getArgument($name)
1018  {
1019  $value = GeneralUtility::_GP($name);
1020 
1021  switch ($name) {
1022  case 'element':
1023  if ($value !== '' && !preg_match('#^[a-z0-9_.]+:[0-9]+$#i', $value)) {
1024  $value = '';
1025  }
1026  break;
1027  case 'rollbackFields':
1028  case 'revert':
1029  if ($value !== '' && !preg_match('#^[a-z0-9_.]+(:[0-9]+(:[a-z0-9_.]+)?)?$#i', $value)) {
1030  $value = '';
1031  }
1032  break;
1033  case 'returnUrl':
1034  $value = GeneralUtility::sanitizeLocalUrl($value);
1035  break;
1036  case 'diff':
1037  case 'highlight':
1038  case 'sh_uid':
1039  $value = (int)$value;
1040  break;
1041  case 'settings':
1042  if (!is_array($value)) {
1043  $value = array();
1044  }
1045  break;
1046  default:
1047  $value = '';
1048  }
1049 
1050  return $value;
1051  }
1052 
1056  protected function getLanguageService()
1057  {
1058  return $GLOBALS['LANG'];
1059  }
1060 
1064  protected function getDatabaseConnection()
1065  {
1066  return $GLOBALS['TYPO3_DB'];
1067  }
1068 }