‪TYPO3CMS  11.5
RecordHistory.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use TYPO3\CMS\Backend\Utility\BackendUtility;
22 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
26 
32 {
38  protected ‪$maxSteps = 20;
39 
45  protected ‪$showSubElements = true;
46 
52  protected ‪$element;
53 
59  protected ‪$lastHistoryEntry = 0;
60 
65  protected ‪$pageAccessCache = [];
66 
71  protected ‪$rollbackFields = '';
72 
79  public function ‪__construct(‪$element = '', ‪$rollbackFields = '')
80  {
81  $this->element = $this->‪sanitizeElementValue((string)‪$element);
82  $this->rollbackFields = $this->‪sanitizeRollbackFieldsValue((string)‪$rollbackFields);
83  }
84 
91  {
92  $this->lastHistoryEntry = ‪$lastHistoryEntry;
93  $this->‪updateCurrentElement();
94  }
95 
96  public function ‪getLastHistoryEntryNumber(): int
97  {
99  }
100 
107  public function ‪setMaxSteps(int ‪$maxSteps): void
108  {
109  $this->maxSteps = ‪$maxSteps;
110  }
111 
118  public function ‪setShowSubElements(bool ‪$showSubElements): void
119  {
120  $this->showSubElements = ‪$showSubElements;
121  }
122 
126  public function ‪getChangeLog(): array
127  {
128  if (!empty($this->element)) {
129  [$table, $recordUid] = explode(':', $this->element);
130  return $this->‪getHistoryData($table, (int)$recordUid, $this->showSubElements, $this->lastHistoryEntry);
131  }
132  return [];
133  }
134 
139  public function ‪getElementInformation(): array
140  {
141  return !empty($this->element) ? explode(':', $this->element) : [];
142  }
143 
147  public function ‪getElementString(): string
148  {
149  return (string)‪$this->element;
150  }
151 
152  /*******************************
153  *
154  * build up history
155  *
156  *******************************/
157 
164  public function ‪getDiff(array $changeLog): array
165  {
166  $insertsDeletes = [];
167  $newArr = [];
168  $differences = [];
169  // traverse changelog array
170  foreach ($changeLog as $value) {
171  $field = $value['tablename'] . ':' . $value['recuid'];
172  // inserts / deletes
173  if ((int)$value['actiontype'] !== ‪RecordHistoryStore::ACTION_MODIFY) {
174  if (!isset($insertsDeletes[$field])) {
175  $insertsDeletes[$field] = 0;
176  }
177  ($value['action'] ?? '') === 'insert' ? $insertsDeletes[$field]++ : $insertsDeletes[$field]--;
178  // unset not needed fields
179  if ($insertsDeletes[$field] === 0) {
180  unset($insertsDeletes[$field]);
181  }
182  } elseif (!isset($newArr[$field])) {
183  $newArr[$field] = $value['newRecord'];
184  $differences[$field] = $value['oldRecord'];
185  } else {
186  $differences[$field] = array_merge($differences[$field], $value['oldRecord']);
187  }
188  }
189  // remove entries where there were no changes effectively
190  foreach ($newArr as $record => $value) {
191  foreach ($value as $key => $innerVal) {
192  if ($newArr[$record][$key] === $differences[$record][$key]) {
193  unset($newArr[$record][$key], $differences[$record][$key]);
194  }
195  }
196  if (empty($newArr[$record]) && empty($differences[$record])) {
197  unset($newArr[$record], $differences[$record]);
198  }
199  }
200  return [
201  'newData' => $newArr,
202  'oldData' => $differences,
203  'insertsDeletes' => $insertsDeletes,
204  ];
205  }
206 
216  protected function ‪getHistoryData(string $table, int $uid, bool $includeSubEntries = null, int ‪$lastHistoryEntry = null): array
217  {
218  $historyDataForRecord = $this->‪getHistoryDataForRecord($table, $uid, ‪$lastHistoryEntry);
219  // get history of tables of this page and merge it into changelog
220  if ($table === 'pages' && $includeSubEntries && $this->‪hasPageAccess('pages', $uid)) {
221  foreach (‪$GLOBALS['TCA'] as $tablename => $value) {
222  // check if there are records on the page
223  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tablename);
224  $queryBuilder->getRestrictions()->removeAll();
225 
226  $result = $queryBuilder
227  ->select('uid')
228  ->from($tablename)
229  ->where(
230  $queryBuilder->expr()->eq(
231  'pid',
232  $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT)
233  )
234  )
235  ->executeQuery();
236  while ($row = $result->fetchAssociative()) {
237  // if there is history data available, merge it into changelog
238  $newChangeLog = $this->‪getHistoryDataForRecord($tablename, $row['uid'], ‪$lastHistoryEntry);
239  if (is_array($newChangeLog) && !empty($newChangeLog)) {
240  foreach ($newChangeLog as $key => $newChangeLogEntry) {
241  $historyDataForRecord[$key] = $newChangeLogEntry;
242  }
243  }
244  }
245  }
246  }
247  usort($historyDataForRecord, static function (array $a, array $b): int {
248  if ($a['tstamp'] < $b['tstamp']) {
249  return 1;
250  }
251  if ($a['tstamp'] > $b['tstamp']) {
252  return -1;
253  }
254  return 0;
255  });
256  return $historyDataForRecord;
257  }
258 
268  public function ‪getHistoryDataForRecord(string $table, int $uid, int ‪$lastHistoryEntry = null): array
269  {
270  if (empty(‪$GLOBALS['TCA'][$table]) || !$this->‪hasTableAccess($table) || !$this->‪hasPageAccess($table, $uid)) {
271  return [];
272  }
273 
274  $uid = $this->‪resolveElement($table, $uid);
275  return $this->‪findEventsForRecord($table, $uid, ($this->maxSteps ?: 0), ‪$lastHistoryEntry);
276  }
277 
278  /*******************************
279  *
280  * Various helper functions
281  *
282  *******************************/
283 
291  protected function ‪resolveElement(string $table, int $uid): int
292  {
293  if (isset(‪$GLOBALS['TCA'][$table])
294  && $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->‪getBackendUser()->workspace, $table, $uid, 'uid')) {
295  $uid = $workspaceVersion['uid'];
296  }
297  return $uid;
298  }
299 
306  protected function ‪getHistoryEntry(int ‪$lastHistoryEntry): array
307  {
308  $queryBuilder = $this->‪getQueryBuilder();
309  $record = $queryBuilder
310  ->select('uid', 'tablename', 'recuid')
311  ->from('sys_history')
312  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$lastHistoryEntry, ‪Connection::PARAM_INT)))
313  ->executeQuery()
314  ->fetchAssociative();
315 
316  if (empty($record)) {
317  return [];
318  }
319 
320  return $record;
321  }
322 
333  public function ‪findEventsForRecord(string $table, int $uid, int $limit = 0, int $minimumUid = null): array
334  {
335  $backendUser = $this->‪getBackendUser();
336  $queryBuilder = $this->‪getQueryBuilder();
337  $queryBuilder
338  ->select('*')
339  ->from('sys_history')
340  ->where(
341  $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table, ‪Connection::PARAM_STR)),
342  $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter($uid, ‪Connection::PARAM_INT))
343  );
344  if ($backendUser->workspace === 0) {
345  $queryBuilder->andWhere(
346  $queryBuilder->expr()->eq('workspace', 0)
347  );
348  } else {
349  $queryBuilder->andWhere(
350  $queryBuilder->expr()->orX(
351  $queryBuilder->expr()->eq('workspace', 0),
352  $queryBuilder->expr()->eq('workspace', $queryBuilder->createNamedParameter($backendUser->workspace, ‪Connection::PARAM_INT))
353  )
354  );
355  }
356  if ($limit) {
357  $queryBuilder->setMaxResults($limit);
358  }
359 
360  if ($minimumUid) {
361  $queryBuilder->andWhere($queryBuilder->expr()->gte('uid', $queryBuilder->createNamedParameter($minimumUid, ‪Connection::PARAM_INT)));
362  }
363 
364  return $this->‪prepareEventDataFromQueryBuilder($queryBuilder);
365  }
366 
367  public function ‪findEventsForCorrelation(string $correlationId): array
368  {
369  $queryBuilder = $this->‪getQueryBuilder();
370  $queryBuilder
371  ->select('*')
372  ->from('sys_history')
373  ->where($queryBuilder->expr()->eq('correlation_id', $queryBuilder->createNamedParameter($correlationId, ‪Connection::PARAM_STR)));
374 
375  return $this->‪prepareEventDataFromQueryBuilder($queryBuilder);
376  }
377 
378  protected function ‪prepareEventDataFromQueryBuilder(QueryBuilder $queryBuilder): array
379  {
380  $events = [];
381  $result = $queryBuilder->orderBy('tstamp', 'DESC')->executeQuery();
382  while ($row = $result->fetchAssociative()) {
383  $identifier = (int)$row['uid'];
384  $actionType = (int)$row['actiontype'];
385  if ($actionType === ‪RecordHistoryStore::ACTION_ADD || $actionType === ‪RecordHistoryStore::ACTION_UNDELETE) {
386  $row['action'] = 'insert';
387  }
388  if ($actionType === ‪RecordHistoryStore::ACTION_DELETE) {
389  $row['action'] = 'delete';
390  }
391  if ($row['history_data'] === null) {
392  $events[$identifier] = $row;
393  continue;
394  }
395  if (strpos($row['history_data'], 'a') === 0) {
396  // legacy code
397  $row['history_data'] = unserialize($row['history_data'], ['allowed_classes' => false]);
398  } else {
399  $row['history_data'] = json_decode($row['history_data'], true);
400  }
401  if (isset($row['history_data']['newRecord'])) {
402  $row['newRecord'] = $row['history_data']['newRecord'];
403  }
404  if (isset($row['history_data']['oldRecord'])) {
405  $row['oldRecord'] = $row['history_data']['oldRecord'];
406  }
407  $events[$identifier] = $row;
408  }
409  krsort($events);
410  return $events;
411  }
412 
420  protected function ‪hasPageAccess($table, $uid): bool
421  {
422  $pageRecord = null;
423  $uid = (int)$uid;
424 
425  if ($table === 'pages') {
426  $pageId = $uid;
427  } else {
428  $record = BackendUtility::getRecord($table, $uid, '*', '', false);
429  $pageId = ($record['pid'] ?? 0);
430  }
431 
432  if ($pageId === 0 && (‪$GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction'] ?? false)) {
433  return true;
434  }
435 
436  if (!isset($this->pageAccessCache[$pageId])) {
437  $isDeletedPage = false;
438  if (isset(‪$GLOBALS['TCA']['pages']['ctrl']['delete'])) {
439  $deletedField = ‪$GLOBALS['TCA']['pages']['ctrl']['delete'];
440  ‪$fields = 'pid,' . $deletedField;
441  $pageRecord = BackendUtility::getRecord('pages', $pageId, ‪$fields, '', false);
442  $isDeletedPage = (bool)($pageRecord[$deletedField] ?? false);
443  }
444  if ($isDeletedPage) {
445  // The page is deleted, so we fake its uid to be the one of the parent page.
446  // By doing so, the following API will use this id to traverse the rootline
447  // and check whether it is in the users' web mounts.
448  // We check however if the user has (or better had) access to the deleted page itself.
449  // Since the only way we got here is by requesting the history of the parent page
450  // we can be sure this parent page actually exists.
451  $pageRecord['uid'] = $pageRecord['pid'];
452  $this->pageAccessCache[$pageId] = $this->‪getBackendUser()->doesUserHaveAccess($pageRecord, ‪Permission::PAGE_SHOW);
453  } else {
454  $this->pageAccessCache[$pageId] = BackendUtility::readPageAccess(
455  $pageId,
456  $this->‪getBackendUser()->getPagePermsClause(‪Permission::PAGE_SHOW)
457  );
458  }
459  }
460 
461  return $this->pageAccessCache[$pageId] !== false;
462  }
463 
471  protected function ‪sanitizeElementValue(string $value): string
472  {
473  if ($value !== '' && !preg_match('#^[a-z\d_.]+:[\d]+$#i', $value)) {
474  return '';
475  }
476  return $value;
477  }
478 
485  protected function ‪sanitizeRollbackFieldsValue(string $value): string
486  {
487  if ($value !== '' && !preg_match('#^[a-z\d_.]+(:[\d]+(:[a-z\d_.]+)?)?$#i', $value)) {
488  return '';
489  }
490  return $value;
491  }
492 
499  protected function ‪hasTableAccess($table): bool
500  {
501  return $this->‪getBackendUser()->check('tables_select', $table);
502  }
503 
504  protected function ‪getBackendUser(): ‪BackendUserAuthentication
505  {
506  return ‪$GLOBALS['BE_USER'];
507  }
508 
509  protected function ‪getQueryBuilder(): QueryBuilder
510  {
511  return GeneralUtility::makeInstance(ConnectionPool::class)
512  ->getQueryBuilderForTable('sys_history');
513  }
514 
515  protected function ‪updateCurrentElement(): void
516  {
517  if ($this->lastHistoryEntry) {
518  $elementData = $this->‪getHistoryEntry($this->lastHistoryEntry);
519  if (!empty($elementData) && empty($this->element)) {
520  $this->element = $elementData['tablename'] . ':' . $elementData['recuid'];
521  }
522  }
523  }
524 }
‪TYPO3\CMS\Backend\History\RecordHistory\getElementString
‪string getElementString()
Definition: RecordHistory.php:141
‪TYPO3\CMS\Backend\History\RecordHistory\getDiff
‪array getDiff(array $changeLog)
Definition: RecordHistory.php:158
‪TYPO3\CMS\Backend\History\RecordHistory\getChangeLog
‪getChangeLog()
Definition: RecordHistory.php:120
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryEntry
‪array getHistoryEntry(int $lastHistoryEntry)
Definition: RecordHistory.php:300
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Backend\History\RecordHistory\hasPageAccess
‪bool hasPageAccess($table, $uid)
Definition: RecordHistory.php:414
‪TYPO3\CMS\Backend\History\RecordHistory\$rollbackFields
‪string $rollbackFields
Definition: RecordHistory.php:65
‪TYPO3\CMS\Backend\History\RecordHistory\setShowSubElements
‪setShowSubElements(bool $showSubElements)
Definition: RecordHistory.php:112
‪TYPO3\CMS\Backend\History\RecordHistory\sanitizeRollbackFieldsValue
‪string sanitizeRollbackFieldsValue(string $value)
Definition: RecordHistory.php:479
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_MODIFY
‪const ACTION_MODIFY
Definition: RecordHistoryStore.php:33
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:54
‪TYPO3\CMS\Backend\History\RecordHistory\resolveElement
‪int resolveElement(string $table, int $uid)
Definition: RecordHistory.php:285
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryDataForRecord
‪array getHistoryDataForRecord(string $table, int $uid, int $lastHistoryEntry=null)
Definition: RecordHistory.php:262
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore
Definition: RecordHistoryStore.php:31
‪TYPO3\CMS\Backend\History
‪TYPO3\CMS\Backend\History\RecordHistory\findEventsForRecord
‪array findEventsForRecord(string $table, int $uid, int $limit=0, int $minimumUid=null)
Definition: RecordHistory.php:327
‪TYPO3\CMS\Backend\History\RecordHistory\sanitizeElementValue
‪string sanitizeElementValue(string $value)
Definition: RecordHistory.php:465
‪TYPO3\CMS\Backend\History\RecordHistory\setLastHistoryEntryNumber
‪setLastHistoryEntryNumber(int $lastHistoryEntry)
Definition: RecordHistory.php:84
‪TYPO3\CMS\Backend\History\RecordHistory
Definition: RecordHistory.php:32
‪TYPO3\CMS\Backend\History\RecordHistory\setMaxSteps
‪setMaxSteps(int $maxSteps)
Definition: RecordHistory.php:101
‪TYPO3\CMS\Backend\History\RecordHistory\$showSubElements
‪bool $showSubElements
Definition: RecordHistory.php:43
‪TYPO3\CMS\Backend\History\RecordHistory\getElementInformation
‪array getElementInformation()
Definition: RecordHistory.php:133
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:62
‪TYPO3\CMS\Backend\History\RecordHistory\$pageAccessCache
‪array $pageAccessCache
Definition: RecordHistory.php:60
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Backend\History\RecordHistory\getQueryBuilder
‪getQueryBuilder()
Definition: RecordHistory.php:503
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryData
‪array getHistoryData(string $table, int $uid, bool $includeSubEntries=null, int $lastHistoryEntry=null)
Definition: RecordHistory.php:210
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Backend\History\RecordHistory\hasTableAccess
‪bool hasTableAccess($table)
Definition: RecordHistory.php:493
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_DELETE
‪const ACTION_DELETE
Definition: RecordHistoryStore.php:35
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Backend\History\RecordHistory\getBackendUser
‪getBackendUser()
Definition: RecordHistory.php:498
‪TYPO3\CMS\Backend\History\RecordHistory\getLastHistoryEntryNumber
‪getLastHistoryEntryNumber()
Definition: RecordHistory.php:90
‪TYPO3\CMS\Backend\History\RecordHistory\$element
‪string $element
Definition: RecordHistory.php:49
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_ADD
‪const ACTION_ADD
Definition: RecordHistoryStore.php:32
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Backend\History\RecordHistory\$lastHistoryEntry
‪int $lastHistoryEntry
Definition: RecordHistory.php:55
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_UNDELETE
‪const ACTION_UNDELETE
Definition: RecordHistoryStore.php:36
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Backend\History\RecordHistory\__construct
‪__construct($element='', $rollbackFields='')
Definition: RecordHistory.php:73
‪TYPO3\CMS\Backend\History\RecordHistory\$maxSteps
‪int $maxSteps
Definition: RecordHistory.php:37
‪TYPO3\CMS\Backend\History\RecordHistory\findEventsForCorrelation
‪findEventsForCorrelation(string $correlationId)
Definition: RecordHistory.php:361
‪TYPO3\CMS\Backend\History\RecordHistory\prepareEventDataFromQueryBuilder
‪prepareEventDataFromQueryBuilder(QueryBuilder $queryBuilder)
Definition: RecordHistory.php:372
‪TYPO3\CMS\Backend\History\RecordHistory\updateCurrentElement
‪updateCurrentElement()
Definition: RecordHistory.php:509