TYPO3 CMS  TYPO3_8-7
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 $rollbackFields = '';
103 
107  protected $iconFactory;
108 
112  protected $view;
113 
117  public function __construct()
118  {
119  $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
120  // GPvars:
121  $this->element = $this->getArgument('element');
122  $this->returnUrl = $this->getArgument('returnUrl');
123  $this->lastSyslogId = $this->getArgument('diff');
124  $this->rollbackFields = $this->getArgument('rollbackFields');
125  // Resolve sh_uid if set
126  $this->resolveShUid();
127 
128  $this->view = $this->getFluidTemplateObject();
129  }
130 
137  public function main()
138  {
139  // Save snapshot
140  if ($this->getArgument('highlight') && !$this->getArgument('settings')) {
141  $this->toggleHighlight($this->getArgument('highlight'));
142  }
143 
144  $this->displaySettings();
145 
146  if ($this->createChangeLog()) {
147  if ($this->rollbackFields) {
148  $completeDiff = $this->createMultipleDiff();
149  $this->performRollback($completeDiff);
150  }
151  if ($this->lastSyslogId) {
152  $this->view->assign('lastSyslogId', $this->lastSyslogId);
153  $completeDiff = $this->createMultipleDiff();
154  $this->displayMultipleDiff($completeDiff);
155  }
156  if ($this->element) {
157  $this->displayHistory();
158  }
159  }
160 
161  return $this->view->render();
162  }
163 
164  /*******************************
165  *
166  * database actions
167  *
168  *******************************/
174  public function toggleHighlight($uid)
175  {
176  $uid = (int)$uid;
177  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
178  $row = $queryBuilder
179  ->select('snapshot')
180  ->from('sys_history')
181  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
182  ->execute()
183  ->fetch();
184 
185  if (!empty($row)) {
186  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
187  $queryBuilder
188  ->update('sys_history')
189  ->set('snapshot', (int)!$row['snapshot'])
190  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)))
191  ->execute();
192  }
193  }
194 
202  public function performRollback($diff)
203  {
204  if (!$this->rollbackFields) {
205  return '';
206  }
207  $reloadPageFrame = 0;
208  $rollbackData = explode(':', $this->rollbackFields);
209  // PROCESS INSERTS AND DELETES
210  // rewrite inserts and deletes
211  $cmdmapArray = [];
212  $data = [];
213  if ($diff['insertsDeletes']) {
214  switch (count($rollbackData)) {
215  case 1:
216  // all tables
217  $data = $diff['insertsDeletes'];
218  break;
219  case 2:
220  // one record
221  if ($diff['insertsDeletes'][$this->rollbackFields]) {
222  $data[$this->rollbackFields] = $diff['insertsDeletes'][$this->rollbackFields];
223  }
224  break;
225  case 3:
226  // one field in one record -- ignore!
227  break;
228  }
229  if (!empty($data)) {
230  foreach ($data as $key => $action) {
231  $elParts = explode(':', $key);
232  if ((int)$action === 1) {
233  // inserted records should be deleted
234  $cmdmapArray[$elParts[0]][$elParts[1]]['delete'] = 1;
235  // When the record is deleted, the contents of the record do not need to be updated
236  unset($diff['oldData'][$key]);
237  unset($diff['newData'][$key]);
238  } elseif ((int)$action === -1) {
239  // deleted records should be inserted again
240  $cmdmapArray[$elParts[0]][$elParts[1]]['undelete'] = 1;
241  }
242  }
243  }
244  }
245  // Writes the data:
246  if ($cmdmapArray) {
247  $tce = GeneralUtility::makeInstance(DataHandler::class);
248  $tce->debug = 0;
249  $tce->dontProcessTransformations = 1;
250  $tce->start([], $cmdmapArray);
251  $tce->process_cmdmap();
252  unset($tce);
253  if (isset($cmdmapArray['pages'])) {
254  $reloadPageFrame = 1;
255  }
256  }
257  if (!$diff['insertsDeletes']) {
258  // PROCESS CHANGES
259  // create an array for process_datamap
260  $diffModified = [];
261  foreach ($diff['oldData'] as $key => $value) {
262  $splitKey = explode(':', $key);
263  $diffModified[$splitKey[0]][$splitKey[1]] = $value;
264  }
265  switch (count($rollbackData)) {
266  case 1:
267  // all tables
268  $data = $diffModified;
269  break;
270  case 2:
271  // one record
272  $data[$rollbackData[0]][$rollbackData[1]] = $diffModified[$rollbackData[0]][$rollbackData[1]];
273  break;
274  case 3:
275  // one field in one record
276  $data[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]] = $diffModified[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]];
277  break;
278  }
279  // Removing fields:
280  $data = $this->removeFilefields($rollbackData[0], $data);
281  // Writes the data:
282  $tce = GeneralUtility::makeInstance(DataHandler::class);
283  $tce->debug = 0;
284  $tce->dontProcessTransformations = 1;
285  $tce->start($data, []);
286  $tce->process_datamap();
287  unset($tce);
288  if (isset($data['pages'])) {
289  $reloadPageFrame = 1;
290  }
291  }
292  // Return to normal operation
293  $this->lastSyslogId = false;
294  $this->rollbackFields = '';
295  $this->createChangeLog();
296  $this->view->assign('reloadPageFrame', $reloadPageFrame);
297  }
298 
299  /*******************************
300  *
301  * Display functions
302  *
303  *******************************/
307  public function displaySettings()
308  {
309  // Get current selection from UC, merge data, write it back to UC
310  $currentSelection = is_array($this->getBackendUser()->uc['moduleData']['history'])
311  ? $this->getBackendUser()->uc['moduleData']['history']
312  : ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1, 'showInsertDelete' => 1];
313  $currentSelectionOverride = $this->getArgument('settings');
314  if ($currentSelectionOverride) {
315  $currentSelection = array_merge($currentSelection, $currentSelectionOverride);
316  $this->getBackendUser()->uc['moduleData']['history'] = $currentSelection;
317  $this->getBackendUser()->writeUC($this->getBackendUser()->uc);
318  }
319  // Display selector for number of history entries
320  $selector['maxSteps'] = [
321  10 => [
322  'value' => 10
323  ],
324  20 => [
325  'value' => 20
326  ],
327  50 => [
328  'value' => 50
329  ],
330  100 => [
331  'value' => 100
332  ],
333  999 => [
334  'value' => 'maxSteps_all'
335  ],
336  'marked' => [
337  'value' => 'maxSteps_marked'
338  ]
339  ];
340  $selector['showDiff'] = [
341  0 => [
342  'value' => 'showDiff_no'
343  ],
344  1 => [
345  'value' => 'showDiff_inline'
346  ]
347  ];
348  $selector['showSubElements'] = [
349  0 => [
350  'value' => 'no'
351  ],
352  1 => [
353  'value' => 'yes'
354  ]
355  ];
356  $selector['showInsertDelete'] = [
357  0 => [
358  'value' => 'no'
359  ],
360  1 => [
361  'value' => 'yes'
362  ]
363  ];
364 
365  $scriptUrl = GeneralUtility::linkThisScript();
366  $languageService = $this->getLanguageService();
367 
368  foreach ($selector as $key => $values) {
369  foreach ($values as $singleKey => $singleVal) {
370  $selector[$key][$singleKey]['scriptUrl'] = htmlspecialchars(GeneralUtility::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey));
371  }
372  }
373  $this->view->assign('settings', $selector);
374  $this->view->assign('currentSelection', $currentSelection);
375  $this->view->assign('TYPO3_REQUEST_URI', htmlspecialchars(GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')));
376 
377  // set values correctly
378  if ($currentSelection['maxSteps'] !== 'marked') {
379  $this->maxSteps = $currentSelection['maxSteps'] ? (int)$currentSelection['maxSteps'] : $this->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 
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  $this->view->assign('singleElement', 'true');
392  $pid = $this->getRecord($elParts[0], $elParts[1]);
393 
394  if ($this->hasPageAccess('pages', $pid['pid'])) {
395  $this->view->assign('fullHistoryLink', $this->linkPage(htmlspecialchars($languageService->getLL('elementHistory_link')), ['element' => 'pages:' . $pid['pid']]));
396  }
397  }
398  }
399 
405  public function displayHistory()
406  {
407  if (empty($this->changeLog)) {
408  return '';
409  }
410  $languageService = $this->getLanguageService();
411  $lines = [];
412  $beUserArray = BackendUtility::getUserNames();
413 
414  $i = 0;
415 
416  // Traverse changeLog array:
417  foreach ($this->changeLog as $sysLogUid => $entry) {
418  // stop after maxSteps
419  if ($this->maxSteps && $i > $this->maxSteps) {
420  break;
421  }
422  // Show only marked states
423  if (!$entry['snapshot'] && $this->showMarked) {
424  continue;
425  }
426  $i++;
427  // Build up single line
428  $singleLine = [];
429 
430  // Get user names
431  $userName = $entry['user'] ? $beUserArray[$entry['user']]['username'] : $languageService->getLL('externalChange');
432  // Executed by switch-user
433  if (!empty($entry['originalUser'])) {
434  $userName .= ' (' . $languageService->getLL('viaUser') . ' ' . $beUserArray[$entry['originalUser']]['username'] . ')';
435  }
436  $singleLine['backendUserName'] = htmlspecialchars($userName);
437  $singleLine['backendUserUid'] = $entry['user'];
438  // add user name
439 
440  // Diff link
441  $image = $this->iconFactory->getIcon('actions-document-history-open', Icon::SIZE_SMALL)->render();
442  $singleLine['rollbackLink']= $this->linkPage($image, ['diff' => $sysLogUid]);
443  // remove first link
444  $singleLine['time'] = htmlspecialchars(BackendUtility::datetime($entry['tstamp']));
445  // add time
446  $singleLine['age'] = htmlspecialchars(BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')));
447  // add age
448 
449  $singleLine['tableUid'] = $this->linkPage(
450  $this->generateTitle($entry['tablename'], $entry['recuid']),
451  ['element' => $entry['tablename'] . ':' . $entry['recuid']],
452  '',
453  htmlspecialchars($languageService->getLL('linkRecordHistory'))
454  );
455  // add record UID
456  // Show insert/delete/diff/changed field names
457  if ($entry['action']) {
458  // insert or delete of element
459  $singleLine['action'] = htmlspecialchars($languageService->getLL($entry['action']));
460  } else {
461  // Display field names instead of full diff
462  if (!$this->showDiff) {
463  // Re-write field names with labels
464  $tmpFieldList = explode(',', $entry['fieldlist']);
465  foreach ($tmpFieldList as $key => $value) {
466  $tmp = str_replace(':', '', htmlspecialchars($languageService->sL(BackendUtility::getItemLabel($entry['tablename'], $value))));
467  if ($tmp) {
468  $tmpFieldList[$key] = $tmp;
469  } else {
470  // remove fields if no label available
471  unset($tmpFieldList[$key]);
472  }
473  }
474  $singleLine['fieldNames'] = htmlspecialchars(implode(',', $tmpFieldList));
475  } else {
476  // Display diff
477  $diff = $this->renderDiff($entry, $entry['tablename']);
478  $singleLine['differences'] = $diff;
479  }
480  }
481  // Show link to mark/unmark state
482  if (!$entry['action']) {
483  if ($entry['snapshot']) {
484  $title = htmlspecialchars($languageService->getLL('unmarkState'));
485  $image = $this->iconFactory->getIcon('actions-unmarkstate', Icon::SIZE_SMALL)->render();
486  } else {
487  $title = htmlspecialchars($languageService->getLL('markState'));
488  $image = $this->iconFactory->getIcon('actions-markstate', Icon::SIZE_SMALL)->render();
489  }
490  $singleLine['markState'] = $this->linkPage($image, ['highlight' => $entry['uid']], '', $title);
491  } else {
492  $singleLine['markState'] = '';
493  }
494  // put line together
495  $lines[] = $singleLine;
496  }
497  $this->view->assign('history', $lines);
498 
499  if ($this->lastSyslogId) {
500  $this->view->assign('fullViewLink', $this->linkPage(htmlspecialchars($languageService->getLL('fullView')), ['diff' => '']));
501  }
502  }
503 
509  public function displayMultipleDiff($diff)
510  {
511  // Get all array keys needed
512  $arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData']));
513  $arrayKeys = array_unique($arrayKeys);
514  $languageService = $this->getLanguageService();
515  if ($arrayKeys) {
516  $lines = [];
517  foreach ($arrayKeys as $key) {
518  $singleLine = [];
519  $elParts = explode(':', $key);
520  // Turn around diff because it should be a "rollback preview"
521  if ((int)$diff['insertsDeletes'][$key] === 1) {
522  // insert
523  $singleLine['insertDelete'] = 'delete';
524  } elseif ((int)$diff['insertsDeletes'][$key] === -1) {
525  $singleLine['insertDelete'] = 'insert';
526  }
527  // Build up temporary diff array
528  // turn around diff because it should be a "rollback preview"
529  if ($diff['newData'][$key]) {
530  $tmpArr['newRecord'] = $diff['oldData'][$key];
531  $tmpArr['oldRecord'] = $diff['newData'][$key];
532  $singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], $elParts[1]);
533  }
534  $elParts = explode(':', $key);
535  $singleLine['revertRecordLink'] = $this->createRollbackLink($key, htmlspecialchars($languageService->getLL('revertRecord')), 1);
536  $singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]);
537  $lines[] = $singleLine;
538  }
539  $this->view->assign('revertAllLink', $this->createRollbackLink('ALL', htmlspecialchars($languageService->getLL('revertAll')), 0));
540  $this->view->assign('multipleDiff', $lines);
541  }
542  }
543 
553  public function renderDiff($entry, $table, $rollbackUid = 0)
554  {
555  $lines = [];
556  if (is_array($entry['newRecord'])) {
557  /* @var DiffUtility $diffUtility */
558  $diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
559  $diffUtility->stripTags = false;
560  $fieldsToDisplay = array_keys($entry['newRecord']);
561  $languageService = $this->getLanguageService();
562  foreach ($fieldsToDisplay as $fN) {
563  if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
564  // Create diff-result:
565  $diffres = $diffUtility->makeDiffDisplay(
566  BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true),
567  BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true)
568  );
569  $lines[] = [
570  'title' => ($rollbackUid ? $this->createRollbackLink(($table . ':' . $rollbackUid . ':' . $fN), htmlspecialchars($languageService->getLL('revertField')), 2) : '') . '
571  ' . htmlspecialchars($languageService->sL(BackendUtility::getItemLabel($table, $fN))),
572  'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres))
573  ];
574  }
575  }
576  }
577  if ($lines) {
578  return $lines;
579  }
580  // error fallback
581  return null;
582  }
583 
584  /*******************************
585  *
586  * build up history
587  *
588  *******************************/
594  public function createMultipleDiff()
595  {
596  $insertsDeletes = [];
597  $newArr = [];
598  $differences = [];
599  if (!$this->changeLog) {
600  return 0;
601  }
602  // traverse changelog array
603  foreach ($this->changeLog as $value) {
604  $field = $value['tablename'] . ':' . $value['recuid'];
605  // inserts / deletes
606  if ($value['action']) {
607  if (!$insertsDeletes[$field]) {
608  $insertsDeletes[$field] = 0;
609  }
610  if ($value['action'] === 'insert') {
611  $insertsDeletes[$field]++;
612  } else {
613  $insertsDeletes[$field]--;
614  }
615  // unset not needed fields
616  if ($insertsDeletes[$field] === 0) {
617  unset($insertsDeletes[$field]);
618  }
619  } else {
620  // update fields
621  // first row of field
622  if (!isset($newArr[$field])) {
623  $newArr[$field] = $value['newRecord'];
624  $differences[$field] = $value['oldRecord'];
625  } else {
626  // standard
627  $differences[$field] = array_merge($differences[$field], $value['oldRecord']);
628  }
629  }
630  }
631  // remove entries where there were no changes effectively
632  foreach ($newArr as $record => $value) {
633  foreach ($value as $key => $innerVal) {
634  if ($newArr[$record][$key] == $differences[$record][$key]) {
635  unset($newArr[$record][$key]);
636  unset($differences[$record][$key]);
637  }
638  }
639  if (empty($newArr[$record]) && empty($differences[$record])) {
640  unset($newArr[$record]);
641  unset($differences[$record]);
642  }
643  }
644  return [
645  'newData' => $newArr,
646  'oldData' => $differences,
647  'insertsDeletes' => $insertsDeletes
648  ];
649  }
650 
656  public function createChangeLog()
657  {
658  $elParts = explode(':', $this->element);
659 
660  if (empty($this->element)) {
661  return 0;
662  }
663 
664  $changeLog = $this->getHistoryData($elParts[0], $elParts[1]);
665  // get history of tables of this page and merge it into changelog
666  if ($elParts[0] === 'pages' && $this->showSubElements && $this->hasPageAccess('pages', $elParts[1])) {
667  foreach ($GLOBALS['TCA'] as $tablename => $value) {
668  // check if there are records on the page
669  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tablename);
670  $queryBuilder->getRestrictions()->removeAll();
671 
672  $rows = $queryBuilder
673  ->select('uid')
674  ->from($tablename)
675  ->where(
676  $queryBuilder->expr()->eq(
677  'pid',
678  $queryBuilder->createNamedParameter($elParts[1], \PDO::PARAM_INT)
679  )
680  )
681  ->execute();
682  if ($rows->rowCount() === 0) {
683  continue;
684  }
685  foreach ($rows as $row) {
686  // if there is history data available, merge it into changelog
687  $newChangeLog = $this->getHistoryData($tablename, $row['uid']);
688  if (is_array($newChangeLog) && !empty($newChangeLog)) {
689  foreach ($newChangeLog as $key => $newChangeLogEntry) {
690  $changeLog[$key] = $newChangeLogEntry;
691  }
692  }
693  }
694  }
695  }
696  if (!$changeLog) {
697  return 0;
698  }
699  krsort($changeLog);
700  $this->changeLog = $changeLog;
701  return 1;
702  }
703 
711  public function getHistoryData($table, $uid)
712  {
713  if (empty($GLOBALS['TCA'][$table]) || !$this->hasTableAccess($table) || !$this->hasPageAccess($table, $uid)) {
714  // error fallback
715  return 0;
716  }
717  // If table is found in $GLOBALS['TCA']:
718  $uid = $this->resolveElement($table, $uid);
719  // Selecting the $this->maxSteps most recent states:
720  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
721  $rows = $queryBuilder
722  ->select('sys_history.*', 'sys_log.userid', 'sys_log.log_data')
723  ->from('sys_history')
724  ->from('sys_log')
725  ->where(
726  $queryBuilder->expr()->eq(
727  'sys_history.sys_log_uid',
728  $queryBuilder->quoteIdentifier('sys_log.uid')
729  ),
730  $queryBuilder->expr()->eq(
731  'sys_history.tablename',
732  $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
733  ),
734  $queryBuilder->expr()->eq(
735  'sys_history.recuid',
736  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
737  )
738  )
739  ->orderBy('sys_log.uid', 'DESC')
740  ->setMaxResults((int)$this->maxSteps)
741  ->execute()
742  ->fetchAll();
743 
744  $changeLog = [];
745  if (!empty($rows)) {
746  // Traversing the result, building up changesArray / changeLog:
747  foreach ($rows as $row) {
748  // Only history until a certain syslog ID needed
749  if ($this->lastSyslogId && $row['sys_log_uid'] < $this->lastSyslogId) {
750  continue;
751  }
752  $hisDat = unserialize($row['history_data']);
753  $logData = unserialize($row['log_data']);
754  if (is_array($hisDat['newRecord']) && is_array($hisDat['oldRecord'])) {
755  // Add information about the history to the changeLog
756  $hisDat['uid'] = $row['uid'];
757  $hisDat['tstamp'] = $row['tstamp'];
758  $hisDat['user'] = $row['userid'];
759  $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']);
760  $hisDat['snapshot'] = $row['snapshot'];
761  $hisDat['fieldlist'] = $row['fieldlist'];
762  $hisDat['tablename'] = $row['tablename'];
763  $hisDat['recuid'] = $row['recuid'];
764  $changeLog[$row['sys_log_uid']] = $hisDat;
765  } else {
766  debug('ERROR: [getHistoryData]');
767  // error fallback
768  return 0;
769  }
770  }
771  }
772  // SELECT INSERTS/DELETES
773  if ($this->showInsertDelete) {
774  // Select most recent inserts and deletes // WITHOUT snapshots
775  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
776  $result = $queryBuilder
777  ->select('uid', 'userid', 'action', 'tstamp', 'log_data')
778  ->from('sys_log')
779  ->where(
780  $queryBuilder->expr()->eq('type', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
781  $queryBuilder->expr()->orX(
782  $queryBuilder->expr()->eq('action', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)),
783  $queryBuilder->expr()->eq('action', $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT))
784  ),
785  $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)),
786  $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
787  )
788  ->orderBy('uid', 'DESC')
789  ->setMaxResults((int)$this->maxSteps)
790  ->execute();
791 
792  // If none are found, nothing more to do
793  if ($result->rowCount() === 0) {
794  return $changeLog;
795  }
796  foreach ($result as $row) {
797  if ($this->lastSyslogId && $row['uid'] < $this->lastSyslogId) {
798  continue;
799  }
800  $hisDat = [];
801  $logData = unserialize($row['log_data']);
802  switch ($row['action']) {
803  case 1:
804  // Insert
805  $hisDat['action'] = 'insert';
806  break;
807  case 3:
808  // Delete
809  $hisDat['action'] = 'delete';
810  break;
811  }
812  $hisDat['tstamp'] = $row['tstamp'];
813  $hisDat['user'] = $row['userid'];
814  $hisDat['originalUser'] = (empty($logData['originalUser']) ? null : $logData['originalUser']);
815  $hisDat['tablename'] = $table;
816  $hisDat['recuid'] = $uid;
817  $changeLog[$row['uid']] = $hisDat;
818  }
819  }
820  return $changeLog;
821  }
822 
823  /*******************************
824  *
825  * Various helper functions
826  *
827  *******************************/
835  public function generateTitle($table, $uid)
836  {
837  $out = $table . ':' . $uid;
838  if ($labelField = $GLOBALS['TCA'][$table]['ctrl']['label']) {
839  $record = $this->getRecord($table, $uid);
840  $out .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')';
841  }
842  return $out;
843  }
844 
853  public function createRollbackLink($key, $alt = '', $type = 0)
854  {
855  return $this->linkPage('<span class="btn btn-default" style="margin-right: 5px;">' . $alt . '</span>', ['rollbackFields' => $key]);
856  }
857 
868  public function linkPage($str, $inparams = [], $anchor = '', $title = '')
869  {
870  // Setting default values based on GET parameters:
871  $params['element'] = $this->element;
872  $params['returnUrl'] = $this->returnUrl;
873  $params['diff'] = $this->lastSyslogId;
874  // Merging overriding values:
875  $params = array_merge($params, $inparams);
876  // Make the link:
877  $link = BackendUtility::getModuleUrl('record_history', $params) . ($anchor ? '#' . $anchor : '');
878  return '<a href="' . htmlspecialchars($link) . '"' . ($title ? ' title="' . $title . '"' : '') . '>' . $str . '</a>';
879  }
880 
890  public function removeFilefields($table, $dataArray)
891  {
892  if ($GLOBALS['TCA'][$table]) {
893  foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $config) {
894  if ($config['config']['type'] === 'group' && $config['config']['internal_type'] === 'file') {
895  unset($dataArray[$field]);
896  }
897  }
898  }
899  return $dataArray;
900  }
901 
909  public function resolveElement($table, $uid)
910  {
911  if (isset($GLOBALS['TCA'][$table])) {
912  if ($workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->getBackendUser()->workspace, $table, $uid, 'uid')) {
913  $uid = $workspaceVersion['uid'];
914  }
915  }
916  return $uid;
917  }
918 
922  public function resolveShUid()
923  {
924  $shUid = $this->getArgument('sh_uid');
925  if (empty($shUid)) {
926  return;
927  }
928  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_history');
929  $record = $queryBuilder
930  ->select('*')
931  ->from('sys_history')
932  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($shUid, \PDO::PARAM_INT)))
933  ->execute()
934  ->fetch();
935 
936  if (empty($record)) {
937  return;
938  }
939  $this->element = $record['tablename'] . ':' . $record['recuid'];
940  $this->lastSyslogId = $record['sys_log_uid'] - 1;
941  }
942 
950  protected function hasPageAccess($table, $uid)
951  {
952  $uid = (int)$uid;
953 
954  if ($table === 'pages') {
955  $pageId = $uid;
956  } else {
957  $record = $this->getRecord($table, $uid);
958  $pageId = $record['pid'];
959  }
960 
961  if (!isset($this->pageAccessCache[$pageId])) {
962  $isDeletedPage = false;
963  if ($this->showInsertDelete && isset($GLOBALS['TCA']['pages']['ctrl']['delete'])) {
964  $deletedField = $GLOBALS['TCA']['pages']['ctrl']['delete'];
965  $pageRecord = $this->getRecord('pages', $pageId);
966  $isDeletedPage = (bool)$pageRecord[$deletedField];
967  }
968  if ($isDeletedPage) {
969  // The page is deleted, so we fake its uid to be the one of the parent page.
970  // By doing so, the following API will use this id to traverse the rootline
971  // and check whether it is in the users' web mounts.
972  // We check however if the user has (or better had) access to the deleted page itself.
973  // Since the only way we got here is by requesting the history of the parent page
974  // we can be sure this parent page actually exists.
975  $pageRecord['uid'] = $pageRecord['pid'];
976  $this->pageAccessCache[$pageId] = $this->getBackendUser()->doesUserHaveAccess($pageRecord, Permission::PAGE_SHOW);
977  } else {
978  $this->pageAccessCache[$pageId] = BackendUtility::readPageAccess(
979  $pageId,
980  $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)
981  );
982  }
983  }
984 
985  return $this->pageAccessCache[$pageId] !== false;
986  }
987 
994  protected function hasTableAccess($table)
995  {
996  return $this->getBackendUser()->check('tables_select', $table);
997  }
998 
1006  protected function getRecord($table, $uid)
1007  {
1008  if (!isset($this->recordCache[$table][$uid])) {
1009  $this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
1010  }
1011  return $this->recordCache[$table][$uid];
1012  }
1013 
1019  protected function getBackendUser()
1020  {
1021  return $GLOBALS['BE_USER'];
1022  }
1023 
1032  protected function getArgument($name)
1033  {
1034  $value = GeneralUtility::_GP($name);
1035 
1036  switch ($name) {
1037  case 'element':
1038  if ($value !== '' && !preg_match('#^[a-z0-9_.]+:[0-9]+$#i', $value)) {
1039  $value = '';
1040  }
1041  break;
1042  case 'rollbackFields':
1043  case 'revert':
1044  if ($value !== '' && !preg_match('#^[a-z0-9_.]+(:[0-9]+(:[a-z0-9_.]+)?)?$#i', $value)) {
1045  $value = '';
1046  }
1047  break;
1048  case 'returnUrl':
1049  $value = GeneralUtility::sanitizeLocalUrl($value);
1050  break;
1051  case 'diff':
1052  case 'highlight':
1053  case 'sh_uid':
1054  $value = (int)$value;
1055  break;
1056  case 'settings':
1057  if (!is_array($value)) {
1058  $value = [];
1059  }
1060  break;
1061  default:
1062  $value = '';
1063  }
1064 
1065  return $value;
1066  }
1067 
1071  protected function getLanguageService()
1072  {
1073  return $GLOBALS['LANG'];
1074  }
1075 
1081  protected function getFluidTemplateObject()
1082  {
1084  $view = GeneralUtility::makeInstance(StandaloneView::class);
1085  $view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
1086  $view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
1087  $view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
1088 
1089  $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html'));
1090 
1091  $view->getRequest()->setControllerExtensionName('Backend');
1092  return $view;
1093  }
1094 }
createRollbackLink($key, $alt='', $type=0)
static readPageAccess($id, $perms_clause)
static getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields=' *')
debug($variable='', $name=' *variable *', $line=' *line *', $file=' *file *', $recursiveDepth=3, $debugLevel='E_DEBUG')
linkPage($str, $inparams=[], $anchor='', $title='')
static calcAge($seconds, $labels='min|hrs|days|yrs|min|hour|day|year')
static getFileAbsFileName($filename, $_=null, $_2=null)
static linkThisScript(array $getParams=[])
static makeInstance($className,... $constructorArguments)
static getUserNames($fields='username, usergroup, usergroup_cached_list, uid', $where='')
static getRecordTitle($table, $row, $prep=false, $forceResult=true)
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)