TYPO3 CMS  TYPO3_7-6
RecordHistory.php
Go to the documentation of this file.
1 <?php
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 
26 
31 {
37  public $maxSteps = 20;
38 
44  public $showDiff = 1;
45 
51  public $showSubElements = 1;
52 
58  public $showInsertDelete = 1;
59 
65  public $element;
66 
72  public $lastSyslogId;
73 
77  public $returnUrl;
78 
82  public $changeLog = [];
83 
87  public $showMarked = false;
88 
92  protected $recordCache = [];
93 
97  protected $pageAccessCache = [];
98 
102  protected $iconFactory;
103 
107  public function __construct()
108  {
109  $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
110  // GPvars:
111  $this->element = $this->getArgument('element');
112  $this->returnUrl = $this->getArgument('returnUrl');
113  $this->lastSyslogId = $this->getArgument('diff');
114  $this->rollbackFields = $this->getArgument('rollbackFields');
115  // Resolve sh_uid if set
116  $this->resolveShUid();
117  }
118 
125  public function main()
126  {
127  $content = '';
128  // Single-click rollback
129  if ($this->getArgument('revert') && $this->getArgument('sumUp')) {
130  $this->rollbackFields = $this->getArgument('revert');
131  $this->showInsertDelete = 0;
132  $this->showSubElements = 0;
133  $element = explode(':', $this->element);
134  $record = $this->getDatabaseConnection()->exec_SELECTgetSingleRow(
135  '*',
136  'sys_history',
137  'tablename=' . $this->getDatabaseConnection()->fullQuoteStr($element[0], 'sys_history') . ' AND recuid=' . (int)$element[1],
138  '',
139  'uid DESC'
140  );
141  $this->lastSyslogId = $record['sys_log_uid'];
142  $this->createChangeLog();
143  $completeDiff = $this->createMultipleDiff();
144  $this->performRollback($completeDiff);
145  HttpUtility::redirect($this->returnUrl);
146  }
147  // Save snapshot
148  if ($this->getArgument('highlight') && !$this->getArgument('settings')) {
149  $this->toggleHighlight($this->getArgument('highlight'));
150  }
151 
152  $content .= $this->displaySettings();
153 
154  if ($this->createChangeLog()) {
155  if ($this->rollbackFields) {
156  $completeDiff = $this->createMultipleDiff();
157  $content .= $this->performRollback($completeDiff);
158  }
159  if ($this->lastSyslogId) {
160  $completeDiff = $this->createMultipleDiff();
161  $content .= $this->displayMultipleDiff($completeDiff);
162  }
163  if ($this->element) {
164  $content .= $this->displayHistory();
165  }
166  }
167  return $content;
168  }
169 
170  /*******************************
171  *
172  * database actions
173  *
174  *******************************/
181  public function toggleHighlight($uid)
182  {
183  $uid = (int)$uid;
184  $row = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('snapshot', 'sys_history', 'uid=' . $uid);
185  if (!empty($row)) {
186  $this->getDatabaseConnection()->exec_UPDATEquery('sys_history', 'uid=' . $uid, ['snapshot' => !$row['snapshot']]);
187  }
188  }
189 
197  public function performRollback($diff)
198  {
199  if (!$this->rollbackFields) {
200  return '';
201  }
202  $reloadPageFrame = 0;
203  $rollbackData = explode(':', $this->rollbackFields);
204  // PROCESS INSERTS AND DELETES
205  // rewrite inserts and deletes
206  $cmdmapArray = [];
207  $data = [];
208  if ($diff['insertsDeletes']) {
209  switch (count($rollbackData)) {
210  case 1:
211  // all tables
212  $data = $diff['insertsDeletes'];
213  break;
214  case 2:
215  // one record
216  if ($diff['insertsDeletes'][$this->rollbackFields]) {
217  $data[$this->rollbackFields] = $diff['insertsDeletes'][$this->rollbackFields];
218  }
219  break;
220  case 3:
221  // one field in one record -- ignore!
222  break;
223  }
224  if (!empty($data)) {
225  foreach ($data as $key => $action) {
226  $elParts = explode(':', $key);
227  if ((int)$action === 1) {
228  // inserted records should be deleted
229  $cmdmapArray[$elParts[0]][$elParts[1]]['delete'] = 1;
230  // When the record is deleted, the contents of the record do not need to be updated
231  unset($diff['oldData'][$key]);
232  unset($diff['newData'][$key]);
233  } elseif ((int)$action === -1) {
234  // deleted records should be inserted again
235  $cmdmapArray[$elParts[0]][$elParts[1]]['undelete'] = 1;
236  }
237  }
238  }
239  }
240  // Writes the data:
241  if ($cmdmapArray) {
242  $tce = GeneralUtility::makeInstance(DataHandler::class);
243  $tce->stripslashes_values = 0;
244  $tce->debug = 0;
245  $tce->dontProcessTransformations = 1;
246  $tce->start([], $cmdmapArray);
247  $tce->process_cmdmap();
248  unset($tce);
249  if (isset($cmdmapArray['pages'])) {
250  $reloadPageFrame = 1;
251  }
252  }
253  // PROCESS CHANGES
254  // create an array for process_datamap
255  $diffModified = [];
256  foreach ($diff['oldData'] as $key => $value) {
257  $splitKey = explode(':', $key);
258  $diffModified[$splitKey[0]][$splitKey[1]] = $value;
259  }
260  switch (count($rollbackData)) {
261  case 1:
262  // all tables
263  $data = $diffModified;
264  break;
265  case 2:
266  // one record
267  $data[$rollbackData[0]][$rollbackData[1]] = $diffModified[$rollbackData[0]][$rollbackData[1]];
268  break;
269  case 3:
270  // one field in one record
271  $data[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]] = $diffModified[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]];
272  break;
273  }
274  // Removing fields:
275  $data = $this->removeFilefields($rollbackData[0], $data);
276  // Writes the data:
277  $tce = GeneralUtility::makeInstance(DataHandler::class);
278  $tce->stripslashes_values = 0;
279  $tce->debug = 0;
280  $tce->dontProcessTransformations = 1;
281  $tce->start($data, []);
282  $tce->process_datamap();
283  unset($tce);
284  if (isset($data['pages'])) {
285  $reloadPageFrame = 1;
286  }
287  // Return to normal operation
288  $this->lastSyslogId = false;
289  $this->rollbackFields = false;
290  $this->createChangeLog();
291  // Reload page frame if necessary
292  if ($reloadPageFrame) {
293  return '<script type="text/javascript">
294  /*<![CDATA[*/
295  if (top.content && top.content.nav_frame && top.content.nav_frame.refresh_nav) {
296  top.content.nav_frame.refresh_nav();
297  }
298  /*]]>*/
299  </script>';
300  }
301  return '';
302  }
303 
304  /*******************************
305  *
306  * Display functions
307  *
308  *******************************/
314  public function displaySettings()
315  {
316  // Get current selection from UC, merge data, write it back to UC
317  $currentSelection = is_array($this->getBackendUser()->uc['moduleData']['history'])
318  ? $this->getBackendUser()->uc['moduleData']['history']
319  : ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1, 'showInsertDelete' => 1];
320  $currentSelectionOverride = $this->getArgument('settings');
321  if ($currentSelectionOverride) {
322  $currentSelection = array_merge($currentSelection, $currentSelectionOverride);
323  $this->getBackendUser()->uc['moduleData']['history'] = $currentSelection;
324  $this->getBackendUser()->writeUC($this->getBackendUser()->uc);
325  }
326  // Display selector for number of history entries
327  $selector['maxSteps'] = [
328  10 => 10,
329  20 => 20,
330  50 => 50,
331  100 => 100,
332  '' => 'maxSteps_all',
333  'marked' => 'maxSteps_marked'
334  ];
335  $selector['showDiff'] = [
336  0 => 'showDiff_no',
337  1 => 'showDiff_inline'
338  ];
339  $selector['showSubElements'] = [
340  0 => 'no',
341  1 => 'yes'
342  ];
343  $selector['showInsertDelete'] = [
344  0 => 'no',
345  1 => 'yes'
346  ];
347  // render selectors
348  $displayCode = '';
349  $scriptUrl = GeneralUtility::linkThisScript();
350  $languageService = $this->getLanguageService();
351  foreach ($selector as $key => $values) {
352  $displayCode .= '<tr><td>' . $languageService->getLL($key, true) . '</td>';
353 
354  $label = ($currentSelection[$key] !== ''
355  ? ($languageService->getLL($selector[$key][$currentSelection[$key]], true) ?: $selector[$key][$currentSelection[$key]])
356  : ($languageService->getLL($selector[$key][$currentSelection[0]], true) ?: $selector[$key][$currentSelection[0]])
357  );
358 
359  $displayCode .= '<td>
360  <div class="btn-group">
361  <button class="btn btn-default dropdown-toggle" type="button" id="copymodeSelector" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
362  ' . $label . '
363  <span class="caret"></span>
364  </button>
365  <ul class="dropdown-menu" aria-labelledby="copymodeSelector">';
366 
367  foreach ($values as $singleKey => $singleVal) {
368  $caption = $languageService->getLL($singleVal, true) ?: htmlspecialchars($singleVal);
369  $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>';
370  }
371 
372  $displayCode .= '
373  </ul>
374  </div>
375  </td></tr>
376  ';
377  }
378  // set values correctly
379  if ($currentSelection['maxSteps'] !== 'marked') {
380  $this->maxSteps = $currentSelection['maxSteps'] ? (int)$currentSelection['maxSteps'] : '';
381  } else {
382  $this->showMarked = true;
383  $this->maxSteps = false;
384  }
385  $this->showDiff = (int)$currentSelection['showDiff'];
386  $this->showSubElements = (int)$currentSelection['showSubElements'];
387  $this->showInsertDelete = (int)$currentSelection['showInsertDelete'];
388  $content = '';
389  // Get link to page history if the element history is shown
390  $elParts = explode(':', $this->element);
391  if (!empty($this->element) && $elParts[0] !== 'pages') {
392  $content .= '<strong>' . $languageService->getLL('elementHistory', true) . '</strong><br />';
393  $pid = $this->getRecord($elParts[0], $elParts[1]);
394 
395  if ($this->hasPageAccess('pages', $pid['pid'])) {
396  $content .= $this->linkPage('<span class="btn btn-default" style="margin-bottom: 5px;">' . $languageService->getLL('elementHistory_link', true) . '</span>', ['element' => 'pages:' . $pid['pid']]);
397  }
398  }
399 
400  $content .= '<a name="settings_head"></a>
401  <form name="settings" action="' . htmlspecialchars(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')) . '" method="post">
402  <div class="row">
403  <div class="col-sm-12 col-md-6 col-lg-4">
404  <div class="panel panel-default">
405  <div class="panel-heading">' . $languageService->getLL('settings', true) . '</div>
406  <table class="table">
407  ' . $displayCode . '
408  </table>
409  </div>
410  </div>
411  </div>
412  </form>
413  ';
414 
415  return '<div>' . $content . '</div>';
416  }
417 
423  public function displayHistory()
424  {
425  if (empty($this->changeLog)) {
426  return '';
427  }
428  $languageService = $this->getLanguageService();
429  $lines = [];
430  // Initialize:
431  $lines[] = '<thead><tr>
432  <th>' . $languageService->getLL('rollback', true) . '</th>
433  <th>' . $languageService->getLL('time', true) . '</th>
434  <th>' . $languageService->getLL('age', true) . '</th>
435  <th>' . $languageService->getLL('user', true) . '</th>
436  <th>' . $languageService->getLL('tableUid', true) . '</th>
437  <th>' . $languageService->getLL('differences', true) . '</th>
438  <th>&nbsp;</th>
439  </tr></thead>';
440  $beUserArray = BackendUtility::getUserNames();
441 
442  $i = 0;
444  $avatar = GeneralUtility::makeInstance(Avatar::class);
445 
446  // Traverse changeLog array:
447  foreach ($this->changeLog as $sysLogUid => $entry) {
448  // stop after maxSteps
449  if ($this->maxSteps && $i > $this->maxSteps) {
450  break;
451  }
452  // Show only marked states
453  if (!$entry['snapshot'] && $this->showMarked) {
454  continue;
455  }
456  $i++;
457  // Get user names
458  // Build up single line
459  $singleLine = [];
460  $userName = $entry['user'] ? $beUserArray[$entry['user']]['username'] : $languageService->getLL('externalChange');
461  // Executed by switch-user
462  if (!empty($entry['originalUser'])) {
463  $userName .= ' (' . $languageService->getLL('viaUser') . ' ' . $beUserArray[$entry['originalUser']]['username'] . ')';
464  }
465 
466  // Diff link
467  $image = '<span title="' . $languageService->getLL('sumUpChanges', true) . '">' . $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render() . '</span>';
468  $singleLine[] = '<span>' . $this->linkPage($image, ['diff' => $sysLogUid]) . '</span>';
469  // remove first link
470  $singleLine[] = htmlspecialchars(BackendUtility::datetime($entry['tstamp']));
471  // add time
472  $singleLine[] = htmlspecialchars(BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.minutesHoursDaysYears')));
473  // add age
474  $userEntry = is_array($beUserArray[$entry['user']]) ? $beUserArray[$entry['user']] : null;
475  $singleLine[] = $avatar->render($userEntry) . ' ' . htmlspecialchars($userName);
476  // add user name
477  $singleLine[] = $this->linkPage(
478  $this->generateTitle($entry['tablename'], $entry['recuid']),
479  ['element' => $entry['tablename'] . ':' . $entry['recuid']],
480  '',
481  $languageService->getLL('linkRecordHistory', true)
482  );
483  // add record UID
484  // Show insert/delete/diff/changed field names
485  if ($entry['action']) {
486  // insert or delete of element
487  $singleLine[] = '<strong>' . htmlspecialchars($languageService->getLL($entry['action'], true)) . '</strong>';
488  } else {
489  // Display field names instead of full diff
490  if (!$this->showDiff) {
491  // Re-write field names with labels
492  $tmpFieldList = explode(',', $entry['fieldlist']);
493  foreach ($tmpFieldList as $key => $value) {
494  $tmp = str_replace(':', '', $languageService->sl(BackendUtility::getItemLabel($entry['tablename'], $value), true));
495  if ($tmp) {
496  $tmpFieldList[$key] = $tmp;
497  } else {
498  // remove fields if no label available
499  unset($tmpFieldList[$key]);
500  }
501  }
502  $singleLine[] = htmlspecialchars(implode(',', $tmpFieldList));
503  } else {
504  // Display diff
505  $diff = $this->renderDiff($entry, $entry['tablename']);
506  $singleLine[] = $diff;
507  }
508  }
509  // Show link to mark/unmark state
510  if (!$entry['action']) {
511  if ($entry['snapshot']) {
512  $title = $languageService->getLL('unmarkState', true);
513  $image = $this->iconFactory->getIcon('actions-unmarkstate', Icon::SIZE_SMALL)->render();
514  } else {
515  $title = $languageService->getLL('markState', true);
516  $image = $this->iconFactory->getIcon('actions-markstate', Icon::SIZE_SMALL)->render();
517  }
518  $singleLine[] = $this->linkPage($image, ['highlight' => $entry['uid']], '', $title);
519  } else {
520  $singleLine[] = '';
521  }
522  // put line together
523  $lines[] = '
524  <tr>
525  <td>' . implode('</td><td>', $singleLine) . '</td>
526  </tr>';
527  }
528 
529  // @TODO: introduce Fluid Standalone view and use callout viewHelper
530  $theCode = '<div class="callout callout-info">'
531  . '<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>'
532  . '<div class="media-body">'
533  . '<p>' . $languageService->getLL('differenceMsg') . '</p>'
534  . ' <div class="callout-body">'
535  . ' </div></div></div></div>';
536 
537  // Finally, put it all together:
538  $theCode .= '
539  <!--
540  History (list):
541  -->
542 
543  <table class="table table-striped table-hover table-vertical-top" id="typo3-history">
544  ' . implode('', $lines) . '
545  </table>';
546  if ($this->lastSyslogId) {
547  $theCode .= '<br />' . $this->linkPage('<span class="btn btn-default">' . $languageService->getLL('fullView', true) . '</span>', ['diff' => '']);
548  }
549 
550  $theCode .= '<br /><br />';
551 
552  // Add the whole content as a module section:
553  return '<h2>' . $languageService->getLL('changes', true) . '</h2><div>' . $theCode . '</div>';
554  }
555 
562  public function displayMultipleDiff($diff)
563  {
564  $content = '';
565  // Get all array keys needed
566  $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData']));
567  $arrayKeys = array_unique($arrayKeys);
568  $languageService = $this->getLanguageService();
569  if ($arrayKeys) {
570  foreach ($arrayKeys as $key) {
571  $record = '';
572  $elParts = explode(':', $key);
573  // Turn around diff because it should be a "rollback preview"
574  if ((int)$diff['insertsDeletes'][$key] === 1) {
575  // insert
576  $record .= '<strong>' . $languageService->getLL('delete', true) . '</strong>';
577  $record .= '<br />';
578  } elseif ((int)$diff['insertsDeletes'][$key] === -1) {
579  $record .= '<strong>' . $languageService->getLL('insert', true) . '</strong>';
580  $record .= '<br />';
581  }
582  // Build up temporary diff array
583  // turn around diff because it should be a "rollback preview"
584  if ($diff['newData'][$key]) {
585  $tmpArr['newRecord'] = $diff['oldData'][$key];
586  $tmpArr['oldRecord'] = $diff['newData'][$key];
587  $record .= $this->renderDiff($tmpArr, $elParts[0], $elParts[1]);
588  }
589  $elParts = explode(':', $key);
590  $titleLine = $this->createRollbackLink($key, $languageService->getLL('revertRecord', true), 1) . $this->generateTitle($elParts[0], $elParts[1]);
591  $record = '<div style="padding-left:10px;border-left:5px solid darkgray;border-bottom:1px dotted darkgray;padding-bottom:2px;">' . $record . '</div>';
592  // $titleLine contains HTML, no htmlspecialchars here.
593  $content .= '<h3>' . $titleLine . '</h3><div>' . $record . '</div>';
594  }
595  $content = $this->createRollbackLink(
596  'ALL',
597  $languageService->getLL('revertAll', true),
598  0
599  ) . '<div style="padding-left:10px;border-left:5px solid darkgray;border-bottom:1px dotted darkgray;padding-bottom:2px;">' . $content . '</div>';
600  } else {
601  $content = $languageService->getLL('noDifferences', true);
602  }
603  return '<h2>' . $languageService->getLL('mergedDifferences', true) . '</h2><div>' . $content . '</div>';
604  }
605 
615  public function renderDiff($entry, $table, $rollbackUid = 0)
616  {
617  $lines = [];
618  if (is_array($entry['newRecord'])) {
619  $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
620  $diffUtility->stripTags = false;
621  $fieldsToDisplay = array_keys($entry['newRecord']);
622  $languageService = $this->getLanguageService();
623  foreach ($fieldsToDisplay as $fN) {
624  if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
625  // Create diff-result:
626  $diffres = $diffUtility->makeDiffDisplay(
627  BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true),
628  BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true)
629  );
630  $lines[] = '
631  <div class="diff-item">
632  <div class="diff-item-title">
633  ' . ($rollbackUid ? $this->createRollbackLink(($table . ':' . $rollbackUid . ':' . $fN), $languageService->getLL('revertField', true), 2) : '') . '
634  ' . $languageService->sl(BackendUtility::getItemLabel($table, $fN), true) . '
635  </div>
636  <div class="diff-item-result">' . str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres)) . '</div>
637  </div>';
638  }
639  }
640  }
641  if ($lines) {
642  return '<div class="diff">' . implode('', $lines) . '</div>';
643  }
644  // error fallback
645  return null;
646  }
647 
648  /*******************************
649  *
650  * build up history
651  *
652  *******************************/
658  public function createMultipleDiff()
659  {
660  $insertsDeletes = [];
661  $newArr = [];
662  $differences = [];
663  if (!$this->changeLog) {
664  return 0;
665  }
666  // traverse changelog array
667  foreach ($this->changeLog as $value) {
668  $field = $value['tablename'] . ':' . $value['recuid'];
669  // inserts / deletes
670  if ($value['action']) {
671  if (!$insertsDeletes[$field]) {
672  $insertsDeletes[$field] = 0;
673  }
674  if ($value['action'] === 'insert') {
675  $insertsDeletes[$field]++;
676  } else {
677  $insertsDeletes[$field]--;
678  }
679  // unset not needed fields
680  if ($insertsDeletes[$field] === 0) {
681  unset($insertsDeletes[$field]);
682  }
683  } else {
684  // update fields
685  // first row of field
686  if (!isset($newArr[$field])) {
687  $newArr[$field] = $value['newRecord'];
688  $differences[$field] = $value['oldRecord'];
689  } else {
690  // standard
691  $differences[$field] = array_merge($differences[$field], $value['oldRecord']);
692  }
693  }
694  }
695  // remove entries where there were no changes effectively
696  foreach ($newArr as $record => $value) {
697  foreach ($value as $key => $innerVal) {
698  if ($newArr[$record][$key] == $differences[$record][$key]) {
699  unset($newArr[$record][$key]);
700  unset($differences[$record][$key]);
701  }
702  }
703  if (empty($newArr[$record]) && empty($differences[$record])) {
704  unset($newArr[$record]);
705  unset($differences[$record]);
706  }
707  }
708  return [
709  'newData' => $newArr,
710  'oldData' => $differences,
711  'insertsDeletes' => $insertsDeletes
712  ];
713  }
714 
720  public function createChangeLog()
721  {
722  $elParts = explode(':', $this->element);
723 
724  if (empty($this->element)) {
725  return 0;
726  }
727 
728  $changeLog = $this->getHistoryData($elParts[0], $elParts[1]);
729  // get history of tables of this page and merge it into changelog
730  if ($elParts[0] == 'pages' && $this->showSubElements && $this->hasPageAccess('pages', $elParts[1])) {
731  foreach ($GLOBALS['TCA'] as $tablename => $value) {
732  // check if there are records on the page
733  $rows = $this->getDatabaseConnection()->exec_SELECTgetRows('uid', $tablename, 'pid=' . (int)$elParts[1]);
734  if (empty($rows)) {
735  continue;
736  }
737  foreach ($rows as $row) {
738  // if there is history data available, merge it into changelog
739  $newChangeLog = $this->getHistoryData($tablename, $row['uid']);
740  if (is_array($newChangeLog) && !empty($newChangeLog)) {
741  foreach ($newChangeLog as $key => $newChangeLogEntry) {
742  $changeLog[$key] = $newChangeLogEntry;
743  }
744  }
745  }
746  }
747  }
748  if (!$changeLog) {
749  return 0;
750  }
751  krsort($changeLog);
752  $this->changeLog = $changeLog;
753  return 1;
754  }
755 
763  public function getHistoryData($table, $uid)
764  {
765  if (empty($GLOBALS['TCA'][$table]) || !$this->hasTableAccess($table) || !$this->hasPageAccess($table, $uid)) {
766  // error fallback
767  return 0;
768  }
769  // If table is found in $GLOBALS['TCA']:
770  $databaseConnection = $this->getDatabaseConnection();
771  $uid = $this->resolveElement($table, $uid);
772  // Selecting the $this->maxSteps most recent states:
773  $rows = $databaseConnection->exec_SELECTgetRows('sys_history.*, sys_log.userid, sys_log.log_data', 'sys_history, sys_log', 'sys_history.sys_log_uid = sys_log.uid
774  AND sys_history.tablename = ' . $databaseConnection->fullQuoteStr($table, 'sys_history') . '
775  AND sys_history.recuid = ' . (int)$uid, '', 'sys_log.uid DESC', $this->maxSteps);
776  $changeLog = [];
777  if (!empty($rows)) {
778  // Traversing the result, building up changesArray / changeLog:
779  foreach ($rows as $row) {
780  // Only history until a certain syslog ID needed
781  if ($this->lastSyslogId && $row['sys_log_uid'] < $this->lastSyslogId) {
782  continue;
783  }
784  $hisDat = unserialize($row['history_data']);
785  $logData = unserialize($row['log_data']);
786  if (is_array($hisDat['newRecord']) && is_array($hisDat['oldRecord'])) {
787  // Add information about the history to the changeLog
788  $hisDat['uid'] = $row['uid'];
789  $hisDat['tstamp'] = $row['tstamp'];
790  $hisDat['user'] = $row['userid'];
791  $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']);
792  $hisDat['snapshot'] = $row['snapshot'];
793  $hisDat['fieldlist'] = $row['fieldlist'];
794  $hisDat['tablename'] = $row['tablename'];
795  $hisDat['recuid'] = $row['recuid'];
796  $changeLog[$row['sys_log_uid']] = $hisDat;
797  } else {
798  debug('ERROR: [getHistoryData]');
799  // error fallback
800  return 0;
801  }
802  }
803  }
804  // SELECT INSERTS/DELETES
805  if ($this->showInsertDelete) {
806  // Select most recent inserts and deletes // WITHOUT snapshots
807  $rows = $databaseConnection->exec_SELECTgetRows('uid, userid, action, tstamp, log_data', 'sys_log', 'type = 1
808  AND (action=1 OR action=3)
809  AND tablename = ' . $databaseConnection->fullQuoteStr($table, 'sys_log') . '
810  AND recuid = ' . (int)$uid, '', 'uid DESC', $this->maxSteps);
811  // If none are found, nothing more to do
812  if (empty($rows)) {
813  return $changeLog;
814  }
815  foreach ($rows as $row) {
816  if ($this->lastSyslogId && $row['uid'] < $this->lastSyslogId) {
817  continue;
818  }
819  $hisDat = [];
820  $logData = unserialize($row['log_data']);
821  switch ($row['action']) {
822  case 1:
823  // Insert
824  $hisDat['action'] = 'insert';
825  break;
826  case 3:
827  // Delete
828  $hisDat['action'] = 'delete';
829  break;
830  }
831  $hisDat['tstamp'] = $row['tstamp'];
832  $hisDat['user'] = $row['userid'];
833  $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']);
834  $hisDat['tablename'] = $table;
835  $hisDat['recuid'] = $uid;
836  $changeLog[$row['uid']] = $hisDat;
837  }
838  }
839  return $changeLog;
840  }
841 
842  /*******************************
843  *
844  * Various helper functions
845  *
846  *******************************/
854  public function generateTitle($table, $uid)
855  {
856  $out = $table . ':' . $uid;
857  if ($labelField = $GLOBALS['TCA'][$table]['ctrl']['label']) {
858  $record = $this->getRecord($table, $uid);
859  $out .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')';
860  }
861  return $out;
862  }
863 
872  public function createRollbackLink($key, $alt = '', $type = 0)
873  {
874  return $this->linkPage('<span class="btn btn-default" style="margin-right: 5px;">' . $alt . '</span>', ['rollbackFields' => $key]);
875  }
876 
887  public function linkPage($str, $inparams = [], $anchor = '', $title = '')
888  {
889  // Setting default values based on GET parameters:
890  $params['element'] = $this->element;
891  $params['returnUrl'] = $this->returnUrl;
892  $params['diff'] = $this->lastSyslogId;
893  // Merging overriding values:
894  $params = array_merge($params, $inparams);
895  // Make the link:
896  $link = BackendUtility::getModuleUrl('record_history', $params) . ($anchor ? '#' . $anchor : '');
897  return '<a href="' . htmlspecialchars($link) . '"' . ($title ? ' title="' . $title . '"' : '') . '>' . $str . '</a>';
898  }
899 
909  public function removeFilefields($table, $dataArray)
910  {
911  if ($GLOBALS['TCA'][$table]) {
912  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
913  if ($config['config']['type'] === 'group' && $config['config']['internal_type'] === 'file') {
914  unset($dataArray[$field]);
915  }
916  }
917  }
918  return $dataArray;
919  }
920 
928  public function resolveElement($table, $uid)
929  {
930  if (isset($GLOBALS['TCA'][$table])) {
931  if ($workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $uid, 'uid')) {
932  $uid = $workspaceVersion['uid'];
933  }
934  }
935  return $uid;
936  }
937 
943  public function resolveShUid()
944  {
945  $shUid = $this->getArgument('sh_uid');
946  if (empty($shUid)) {
947  return;
948  }
949  $record = $this->getDatabaseConnection()->exec_SELECTgetSingleRow('*', 'sys_history', 'uid=' . (int)$shUid);
950  if (empty($record)) {
951  return;
952  }
953  $this->element = $record['tablename'] . ':' . $record['recuid'];
954  $this->lastSyslogId = $record['sys_log_uid'] - 1;
955  }
956 
964  protected function hasPageAccess($table, $uid)
965  {
966  $uid = (int)$uid;
967 
968  if ($table === 'pages') {
969  $pageId = $uid;
970  } else {
971  $record = $this->getRecord($table, $uid);
972  $pageId = $record['pid'];
973  }
974 
975  if (!isset($this->pageAccessCache[$pageId])) {
976  $isDeletedPage = false;
977  if ($this->showInsertDelete && isset($GLOBALS['TCA']['pages']['ctrl']['delete'])) {
978  $deletedField = $GLOBALS['TCA']['pages']['ctrl']['delete'];
979  $pageRecord = $this->getRecord('pages', $pageId);
980  $isDeletedPage = (bool)$pageRecord[$deletedField];
981  }
982  if ($isDeletedPage) {
983  // The page is deleted, so we fake its uid to be the one of the parent page.
984  // By doing so, the following API will use this id to traverse the rootline
985  // and check whether it is in the users' web mounts.
986  // We check however if the user has (or better had) access to the deleted page itself.
987  // Since the only way we got here is by requesting the history of the parent page
988  // we can be sure this parent page actually exists.
989  $pageRecord['uid'] = $pageRecord['pid'];
990  $this->pageAccessCache[$pageId] = $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::PAGE_SHOW);
991  } else {
992  $this->pageAccessCache[$pageId] = BackendUtility::readPageAccess(
993  $pageId,
994  $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
995  );
996  }
997  }
998 
999  return $this->pageAccessCache[$pageId] !== false;
1000  }
1001 
1008  protected function hasTableAccess($table)
1009  {
1010  return $this->getBackendUser()->check('tables_select', $table);
1011  }
1012 
1020  protected function getRecord($table, $uid)
1021  {
1022  if (!isset($this->recordCache[$table][$uid])) {
1023  $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
1024  }
1025  return $this->recordCache[$table][$uid];
1026  }
1027 
1033  protected function getBackendUser()
1034  {
1035  return $GLOBALS['BE_USER'];
1036  }
1037 
1046  protected function getArgument($name)
1047  {
1048  $value = GeneralUtility::_GP($name);
1049 
1050  switch ($name) {
1051  case 'element':
1052  if ($value !== '' && !preg_match('#^[a-z0-9_.]+:[0-9]+$#i', $value)) {
1053  $value = '';
1054  }
1055  break;
1056  case 'rollbackFields':
1057  case 'revert':
1058  if ($value !== '' && !preg_match('#^[a-z0-9_.]+(:[0-9]+(:[a-z0-9_.]+)?)?$#i', $value)) {
1059  $value = '';
1060  }
1061  break;
1062  case 'returnUrl':
1063  $value = GeneralUtility::sanitizeLocalUrl($value);
1064  break;
1065  case 'diff':
1066  case 'highlight':
1067  case 'sh_uid':
1068  $value = (int)$value;
1069  break;
1070  case 'settings':
1071  if (!is_array($value)) {
1072  $value = [];
1073  }
1074  break;
1075  default:
1076  $value = '';
1077  }
1078 
1079  return $value;
1080  }
1081 
1085  protected function getLanguageService()
1086  {
1087  return $GLOBALS['LANG'];
1088  }
1089 
1093  protected function getDatabaseConnection()
1094  {
1095  return $GLOBALS['TYPO3_DB'];
1096  }
1097 }
createRollbackLink($key, $alt='', $type=0)
static readPageAccess($id, $perms_clause)
static getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields=' *')
static getItemLabel($table, $col, $printAllWrap='')
debug($variable='', $name=' *variable *', $line=' *line *', $file=' *file *', $recursiveDepth=3, $debugLevel='E_DEBUG')
linkPage($str, $inparams=[], $anchor='', $title='')
static linkThisScript(array $getParams=[])
static calcAge($seconds, $labels=' min|hrs|days|yrs|min|hour|day|year')
static getUserNames($fields='username, usergroup, usergroup_cached_list, uid', $where='')
static getRecordTitle($table, $row, $prep=false, $forceResult=true)
static redirect($url, $httpStatus=self::HTTP_STATUS_303)
Definition: HttpUtility.php:76
$uid
Definition: server.php:38
static getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
renderDiff($entry, $table, $rollbackUid=0)