‪TYPO3CMS  9.5
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 
24 
30 {
36  protected ‪$maxSteps = 20;
37 
43  protected ‪$showSubElements = 1;
44 
50  protected ‪$element;
51 
58 
62  public ‪$changeLog = [];
63 
68  protected ‪$pageAccessCache = [];
69 
74  protected ‪$rollbackFields = '';
75 
82  public function ‪__construct(‪$element = '', ‪$rollbackFields = '')
83  {
84  $this->element = $this->‪sanitizeElementValue(‪$element);
85  $this->rollbackFields = $this->‪sanitizeRollbackFieldsValue(‪$rollbackFields);
86  }
87 
93  public function ‪setLastHistoryEntry(int ‪$lastHistoryEntry)
94  {
95  if ($lastHistoryEntry) {
96  $elementData = $this->‪getHistoryEntry($lastHistoryEntry);
97  $this->lastHistoryEntry = ‪$lastHistoryEntry;
98  if (!empty($elementData) && empty($this->element)) {
99  $this->element = $elementData['tablename'] . ':' . $elementData['recuid'];
100  }
101  }
102  }
103 
110  public function ‪setMaxSteps(int ‪$maxSteps)
111  {
112  $this->maxSteps = ‪$maxSteps;
113  }
114 
121  public function ‪setShowSubElements(bool ‪$showSubElements)
122  {
123  $this->showSubElements = ‪$showSubElements;
124  }
125 
129  public function ‪createChangeLog()
130  {
131  if (!empty($this->element)) {
132  list($table, $recordUid) = explode(':', $this->element);
133  $this->changeLog = $this->‪getHistoryData($table, $recordUid, $this->showSubElements, $this->lastHistoryEntry);
134  }
135  }
136 
141  public function ‪shouldPerformRollback()
142  {
143  return !empty($this->rollbackFields);
144  }
145 
151  public function ‪getElementData()
152  {
153  return !empty($this->element) ? explode(':', $this->element) : false;
154  }
155 
159  public function ‪getElementString(): string
160  {
161  return (string)‪$this->element;
162  }
163 
167  public function ‪performRollback()
168  {
169  if (!$this->‪shouldPerformRollback()) {
170  return;
171  }
172  $rollbackData = explode(':', $this->rollbackFields);
173  $diff = $this->‪createMultipleDiff();
174  // PROCESS INSERTS AND DELETES
175  // rewrite inserts and deletes
176  $cmdmapArray = [];
177  $data = [];
178  if ($diff['insertsDeletes']) {
179  switch (count($rollbackData)) {
180  case 1:
181  // all tables
182  $data = $diff['insertsDeletes'];
183  break;
184  case 2:
185  // one record
186  if ($diff['insertsDeletes'][$this->rollbackFields]) {
187  $data[‪$this->rollbackFields] = $diff['insertsDeletes'][‪$this->rollbackFields];
188  }
189  break;
190  case 3:
191  // one field in one record -- ignore!
192  break;
193  }
194  if (!empty($data)) {
195  foreach ($data as $key => $action) {
196  $elParts = explode(':', $key);
197  if ((int)$action === 1) {
198  // inserted records should be deleted
199  $cmdmapArray[$elParts[0]][$elParts[1]]['delete'] = 1;
200  // When the record is deleted, the contents of the record do not need to be updated
201  unset($diff['oldData'][$key]);
202  unset($diff['newData'][$key]);
203  } elseif ((int)$action === -1) {
204  // deleted records should be inserted again
205  $cmdmapArray[$elParts[0]][$elParts[1]]['undelete'] = 1;
206  }
207  }
208  }
209  }
210  // Writes the data:
211  if ($cmdmapArray) {
212  $tce = GeneralUtility::makeInstance(DataHandler::class);
213  $tce->dontProcessTransformations = true;
214  $tce->start([], $cmdmapArray);
215  $tce->process_cmdmap();
216  unset($tce);
217  }
218  if (!$diff['insertsDeletes']) {
219  // PROCESS CHANGES
220  // create an array for process_datamap
221  $diffModified = [];
222  foreach ($diff['oldData'] as $key => $value) {
223  $splitKey = explode(':', $key);
224  $diffModified[$splitKey[0]][$splitKey[1]] = $value;
225  }
226  switch (count($rollbackData)) {
227  case 1:
228  // all tables
229  $data = $diffModified;
230  break;
231  case 2:
232  // one record
233  $data[$rollbackData[0]][$rollbackData[1]] = $diffModified[$rollbackData[0]][$rollbackData[1]];
234  break;
235  case 3:
236  // one field in one record
237  $data[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]] = $diffModified[$rollbackData[0]][$rollbackData[1]][$rollbackData[2]];
238  break;
239  }
240  // Removing fields:
241  $data = $this->‪removeFilefields($rollbackData[0], $data);
242  // Writes the data:
243  $tce = GeneralUtility::makeInstance(DataHandler::class);
244  $tce->dontProcessTransformations = true;
245  $tce->start($data, []);
246  $tce->process_datamap();
247  unset($tce);
248  }
249  // Return to normal operation
250  $this->lastHistoryEntry = false;
251  $this->rollbackFields = '';
252  $this->‪createChangeLog();
253  if (isset($data['pages']) || isset($cmdmapArray['pages'])) {
254  ‪BackendUtility::setUpdateSignal('updatePageTree');
255  }
256  }
257 
258  /*******************************
259  *
260  * build up history
261  *
262  *******************************/
263 
269  public function ‪createMultipleDiff(): array
270  {
271  $insertsDeletes = [];
272  $newArr = [];
273  $differences = [];
274  // traverse changelog array
275  foreach ($this->changeLog as $value) {
276  $field = $value['tablename'] . ':' . $value['recuid'];
277  // inserts / deletes
278  if ((int)$value['actiontype'] !== ‪RecordHistoryStore::ACTION_MODIFY) {
279  if (!$insertsDeletes[$field]) {
280  $insertsDeletes[$field] = 0;
281  }
282  if ($value['action'] === 'insert') {
283  $insertsDeletes[$field]++;
284  } else {
285  $insertsDeletes[$field]--;
286  }
287  // unset not needed fields
288  if ($insertsDeletes[$field] === 0) {
289  unset($insertsDeletes[$field]);
290  }
291  } else {
292  // update fields
293  // first row of field
294  if (!isset($newArr[$field])) {
295  $newArr[$field] = $value['newRecord'];
296  $differences[$field] = $value['oldRecord'];
297  } else {
298  // standard
299  $differences[$field] = array_merge($differences[$field], $value['oldRecord']);
300  }
301  }
302  }
303  // remove entries where there were no changes effectively
304  foreach ($newArr as $record => $value) {
305  foreach ($value as $key => $innerVal) {
306  if ($newArr[$record][$key] == $differences[$record][$key]) {
307  unset($newArr[$record][$key]);
308  unset($differences[$record][$key]);
309  }
310  }
311  if (empty($newArr[$record]) && empty($differences[$record])) {
312  unset($newArr[$record]);
313  unset($differences[$record]);
314  }
315  }
316  return [
317  'newData' => $newArr,
318  'oldData' => $differences,
319  'insertsDeletes' => $insertsDeletes
320  ];
321  }
322 
332  public function ‪getHistoryData(string $table, int $uid, bool $includeSubentries = null, int ‪$lastHistoryEntry = null): array
333  {
335  // get history of tables of this page and merge it into changelog
336  if ($table === 'pages' && $includeSubentries && $this->‪hasPageAccess('pages', $uid)) {
337  foreach (‪$GLOBALS['TCA'] as $tablename => $value) {
338  // check if there are records on the page
339  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tablename);
340  $queryBuilder->getRestrictions()->removeAll();
341 
342  $rows = $queryBuilder
343  ->select('uid')
344  ->from($tablename)
345  ->where(
346  $queryBuilder->expr()->eq(
347  'pid',
348  $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
349  )
350  )
351  ->execute();
352  $rowCount = (int)$queryBuilder->count('uid')->execute()->fetchColumn(0);
353  if ($rowCount === 0) {
354  continue;
355  }
356  foreach ($rows as $row) {
357  // if there is history data available, merge it into changelog
358  $newChangeLog = $this->‪getHistoryDataForRecord($tablename, $row['uid'], ‪$lastHistoryEntry);
359  if (is_array($newChangeLog) && !empty($newChangeLog)) {
360  foreach ($newChangeLog as $key => $newChangeLogEntry) {
361  ‪$changeLog[$key] = $newChangeLogEntry;
362  }
363  }
364  }
365  }
366  }
367  usort(‪$changeLog, function ($a, $b) {
368  if ($a['tstamp'] < $b['tstamp']) {
369  return 1;
370  }
371  if ($a['tstamp'] > $b['tstamp']) {
372  return -1;
373  }
374  return 0;
375  });
376  return ‪$changeLog;
377  }
378 
387  public function ‪getHistoryDataForRecord(string $table, int $uid, int ‪$lastHistoryEntry = null): array
388  {
389  if (empty(‪$GLOBALS['TCA'][$table]) || !$this->‪hasTableAccess($table) || !$this->‪hasPageAccess($table, $uid)) {
390  return [];
391  }
392 
393  $uid = $this->‪resolveElement($table, $uid);
394  return $this->‪findEventsForRecord($table, $uid, ($this->maxSteps ?: null), ‪$lastHistoryEntry);
395  }
396 
397  /*******************************
398  *
399  * Various helper functions
400  *
401  *******************************/
402 
412  protected function ‪removeFilefields($table, $dataArray)
413  {
414  // @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Deprecation logged by TcaMigration class.
415  if (‪$GLOBALS['TCA'][$table]) {
416  foreach (‪$GLOBALS['TCA'][$table]['columns'] as $field => $config) {
417  if ($config['config']['type'] === 'group' && $config['config']['internal_type'] === 'file') {
418  unset($dataArray[$field]);
419  }
420  }
421  }
422  return $dataArray;
423  }
424 
432  protected function ‪resolveElement(string $table, int $uid): int
433  {
434  if (isset(‪$GLOBALS['TCA'][$table])
435  && $workspaceVersion = ‪BackendUtility::getWorkspaceVersionOfRecord($this->‪getBackendUser()->workspace, $table, $uid, 'uid')) {
436  $uid = $workspaceVersion['uid'];
437  }
438  return $uid;
439  }
440 
447  public function ‪getHistoryEntry(int ‪$lastHistoryEntry): array
448  {
449  $queryBuilder = $this->‪getQueryBuilder();
450  $record = $queryBuilder
451  ->select('uid', 'tablename', 'recuid')
452  ->from('sys_history')
453  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$lastHistoryEntry, \PDO::PARAM_INT)))
454  ->execute()
455  ->fetch();
456 
457  if (empty($record)) {
458  return [];
459  }
460 
461  return $record;
462  }
463 
474  public function ‪findEventsForRecord(string $table, int $uid, int $limit = 0, int $minimumUid = null): array
475  {
476  $events = [];
477  $queryBuilder = $this->‪getQueryBuilder();
478  $queryBuilder
479  ->select('*')
480  ->from('sys_history')
481  ->where(
482  $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)),
483  $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))
484  );
485 
486  if ($limit) {
487  $queryBuilder->setMaxResults($limit);
488  }
489 
490  if ($minimumUid) {
491  $queryBuilder->andWhere($queryBuilder->expr()->gte('uid', $queryBuilder->createNamedParameter($minimumUid, \PDO::PARAM_INT)));
492  }
493 
494  $result = $queryBuilder->orderBy('tstamp', 'DESC')->execute();
495  while ($row = $result->fetch()) {
496  $identifier = (int)$row['uid'];
497  if ((int)$row['actiontype'] === ‪RecordHistoryStore::ACTION_ADD || (int)$row['actiontype'] === ‪RecordHistoryStore::ACTION_UNDELETE) {
498  $row['action'] = 'insert';
499  }
500  if ((int)$row['actiontype'] === ‪RecordHistoryStore::ACTION_DELETE) {
501  $row['action'] = 'delete';
502  }
503  if (strpos($row['history_data'], 'a') === 0) {
504  // legacy code
505  $row['history_data'] = unserialize($row['history_data'], ['allowed_classes' => false]);
506  } else {
507  $row['history_data'] = json_decode($row['history_data'], true);
508  }
509  if (isset($row['history_data']['newRecord'])) {
510  $row['newRecord'] = $row['history_data']['newRecord'];
511  }
512  if (isset($row['history_data']['oldRecord'])) {
513  $row['oldRecord'] = $row['history_data']['oldRecord'];
514  }
515  $events[$identifier] = $row;
516  }
517  krsort($events);
518  return $events;
519  }
520 
528  protected function ‪hasPageAccess($table, $uid)
529  {
530  $uid = (int)$uid;
531 
532  if ($table === 'pages') {
533  $pageId = $uid;
534  } else {
535  $record = ‪BackendUtility::getRecord($table, $uid, '*', '', false);
536  $pageId = $record['pid'];
537  }
538 
539  if (!isset($this->pageAccessCache[$pageId])) {
540  $isDeletedPage = false;
541  if (isset(‪$GLOBALS['TCA']['pages']['ctrl']['delete'])) {
542  $deletedField = ‪$GLOBALS['TCA']['pages']['ctrl']['delete'];
543  ‪$fields = 'pid,' . $deletedField;
544  $pageRecord = ‪BackendUtility::getRecord('pages', $pageId, ‪$fields, '', false);
545  $isDeletedPage = (bool)$pageRecord[$deletedField];
546  }
547  if ($isDeletedPage) {
548  // The page is deleted, so we fake its uid to be the one of the parent page.
549  // By doing so, the following API will use this id to traverse the rootline
550  // and check whether it is in the users' web mounts.
551  // We check however if the user has (or better had) access to the deleted page itself.
552  // Since the only way we got here is by requesting the history of the parent page
553  // we can be sure this parent page actually exists.
554  $pageRecord['uid'] = $pageRecord['pid'];
555  $this->pageAccessCache[$pageId] = $this->‪getBackendUser()->‪doesUserHaveAccess($pageRecord, ‪Permission::PAGE_SHOW);
556  } else {
557  $this->pageAccessCache[$pageId] = ‪BackendUtility::readPageAccess(
558  $pageId,
559  $this->‪getBackendUser()->getPagePermsClause(‪Permission::PAGE_SHOW)
560  );
561  }
562  }
563 
564  return $this->pageAccessCache[$pageId] !== false;
565  }
566 
575  protected function ‪sanitizeElementValue($value)
576  {
577  if ($value !== '' && !preg_match('#^[a-z0-9_.]+:[0-9]+$#i', $value)) {
578  return '';
579  }
580  return $value;
581  }
582 
589  protected function ‪sanitizeRollbackFieldsValue($value)
590  {
591  if ($value !== '' && !preg_match('#^[a-z0-9_.]+(:[0-9]+(:[a-z0-9_.]+)?)?$#i', $value)) {
592  return '';
593  }
594  return $value;
595  }
596 
603  protected function ‪hasTableAccess($table)
604  {
605  return $this->‪getBackendUser()->‪check('tables_select', $table);
606  }
607 
613  protected function ‪getBackendUser()
614  {
615  return ‪$GLOBALS['BE_USER'];
616  }
617 
621  protected function ‪getQueryBuilder(): ‪QueryBuilder
622  {
623  return GeneralUtility::makeInstance(ConnectionPool::class)
624  ->getQueryBuilderForTable('sys_history');
625  }
626 }
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:81
‪TYPO3\CMS\Backend\History\RecordHistory\getElementString
‪string getElementString()
Definition: RecordHistory.php:152
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryEntry
‪array getHistoryEntry(int $lastHistoryEntry)
Definition: RecordHistory.php:440
‪TYPO3\CMS\Backend\History\RecordHistory\$changeLog
‪array $changeLog
Definition: RecordHistory.php:57
‪TYPO3\CMS\Backend\History\RecordHistory\sanitizeRollbackFieldsValue
‪string sanitizeRollbackFieldsValue($value)
Definition: RecordHistory.php:582
‪TYPO3\CMS\Backend\History\RecordHistory\hasPageAccess
‪bool hasPageAccess($table, $uid)
Definition: RecordHistory.php:521
‪TYPO3\CMS\Backend\Utility\BackendUtility\setUpdateSignal
‪static setUpdateSignal($set='', $params='')
Definition: BackendUtility.php:3164
‪TYPO3\CMS\Backend\History\RecordHistory\$rollbackFields
‪string $rollbackFields
Definition: RecordHistory.php:67
‪TYPO3\CMS\Backend\History\RecordHistory\shouldPerformRollback
‪bool shouldPerformRollback()
Definition: RecordHistory.php:134
‪TYPO3\CMS\Backend\History\RecordHistory\setShowSubElements
‪setShowSubElements(bool $showSubElements)
Definition: RecordHistory.php:114
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication\check
‪bool check($type, $value)
Definition: BackendUserAuthentication.php:648
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_MODIFY
‪const ACTION_MODIFY
Definition: RecordHistoryStore.php:30
‪$fields
‪$fields
Definition: pages.php:4
‪TYPO3\CMS\Backend\History\RecordHistory\resolveElement
‪int resolveElement(string $table, int $uid)
Definition: RecordHistory.php:425
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryDataForRecord
‪array getHistoryDataForRecord(string $table, int $uid, int $lastHistoryEntry=null)
Definition: RecordHistory.php:380
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:23
‪TYPO3\CMS\Core\Database\Query\QueryBuilder
Definition: QueryBuilder.php:47
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore
Definition: RecordHistoryStore.php:28
‪TYPO3\CMS\Backend\History
Definition: RecordHistory.php:2
‪TYPO3\CMS\Backend\History\RecordHistory\createMultipleDiff
‪array createMultipleDiff()
Definition: RecordHistory.php:262
‪TYPO3\CMS\Backend\History\RecordHistory\findEventsForRecord
‪array findEventsForRecord(string $table, int $uid, int $limit=0, int $minimumUid=null)
Definition: RecordHistory.php:467
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryData
‪array getHistoryData(string $table, int $uid, bool $includeSubentries=null, int $lastHistoryEntry=null)
Definition: RecordHistory.php:325
‪TYPO3\CMS\Backend\History\RecordHistory\getQueryBuilder
‪QueryBuilder getQueryBuilder()
Definition: RecordHistory.php:614
‪TYPO3\CMS\Backend\History\RecordHistory
Definition: RecordHistory.php:30
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication\doesUserHaveAccess
‪bool doesUserHaveAccess($row, $perms)
Definition: BackendUserAuthentication.php:331
‪TYPO3\CMS\Backend\History\RecordHistory\getElementData
‪array bool getElementData()
Definition: RecordHistory.php:144
‪TYPO3\CMS\Backend\History\RecordHistory\setMaxSteps
‪setMaxSteps(int $maxSteps)
Definition: RecordHistory.php:103
‪TYPO3\CMS\Backend\History\RecordHistory\createChangeLog
‪createChangeLog()
Definition: RecordHistory.php:122
‪TYPO3\CMS\Backend\History\RecordHistory\$pageAccessCache
‪array $pageAccessCache
Definition: RecordHistory.php:62
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:32
‪TYPO3\CMS\Backend\History\RecordHistory\setLastHistoryEntry
‪setLastHistoryEntry(int $lastHistoryEntry)
Definition: RecordHistory.php:86
‪TYPO3\CMS\Backend\Utility\BackendUtility
Definition: BackendUtility.php:72
‪TYPO3\CMS\Backend\Utility\BackendUtility\getWorkspaceVersionOfRecord
‪static array bool getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields=' *')
Definition: BackendUtility.php:4166
‪TYPO3\CMS\Backend\History\RecordHistory\performRollback
‪performRollback()
Definition: RecordHistory.php:160
‪TYPO3\CMS\Backend\Utility\BackendUtility\getRecord
‪static array null getRecord($table, $uid, $fields=' *', $where='', $useDeleteClause=true)
Definition: BackendUtility.php:130
‪TYPO3\CMS\Backend\History\RecordHistory\hasTableAccess
‪bool hasTableAccess($table)
Definition: RecordHistory.php:596
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_DELETE
‪const ACTION_DELETE
Definition: RecordHistoryStore.php:32
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:5
‪TYPO3\CMS\Backend\History\RecordHistory\$element
‪string $element
Definition: RecordHistory.php:47
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_ADD
‪const ACTION_ADD
Definition: RecordHistoryStore.php:29
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Backend\History\RecordHistory\getBackendUser
‪TYPO3 CMS Core Authentication BackendUserAuthentication getBackendUser()
Definition: RecordHistory.php:606
‪TYPO3\CMS\Backend\History\RecordHistory\$lastHistoryEntry
‪int $lastHistoryEntry
Definition: RecordHistory.php:53
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_UNDELETE
‪const ACTION_UNDELETE
Definition: RecordHistoryStore.php:33
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Backend\History\RecordHistory\__construct
‪__construct($element='', $rollbackFields='')
Definition: RecordHistory.php:75
‪TYPO3\CMS\Backend\History\RecordHistory\sanitizeElementValue
‪array string int sanitizeElementValue($value)
Definition: RecordHistory.php:568
‪TYPO3\CMS\Backend\History\RecordHistory\$maxSteps
‪int $maxSteps
Definition: RecordHistory.php:35
‪TYPO3\CMS\Backend\History\RecordHistory\$showSubElements
‪int $showSubElements
Definition: RecordHistory.php:41
‪TYPO3\CMS\Backend\History\RecordHistory\removeFilefields
‪array removeFilefields($table, $dataArray)
Definition: RecordHistory.php:405
‪TYPO3\CMS\Backend\Utility\BackendUtility\readPageAccess
‪static array bool readPageAccess($id, $perms_clause)
Definition: BackendUtility.php:635