‪TYPO3CMS  ‪main
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 
89  {
90  $this->lastHistoryEntry = ‪$lastHistoryEntry;
91  $this->‪updateCurrentElement();
92  }
93 
94  public function ‪getLastHistoryEntryNumber(): int
95  {
97  }
98 
103  public function ‪setMaxSteps(int ‪$maxSteps): void
104  {
105  $this->maxSteps = ‪$maxSteps;
106  }
107 
112  public function ‪setShowSubElements(bool ‪$showSubElements): void
113  {
114  $this->showSubElements = ‪$showSubElements;
115  }
116 
120  public function ‪getChangeLog(): array
121  {
122  if (!empty($this->element)) {
123  [$table, $recordUid] = explode(':', $this->element);
124  return $this->‪getHistoryData($table, (int)$recordUid, $this->showSubElements, $this->lastHistoryEntry);
125  }
126  return [];
127  }
128 
132  public function ‪getElementInformation(): array
133  {
134  return !empty($this->element) ? explode(':', $this->element) : [];
135  }
136 
140  public function ‪getElementString(): string
141  {
142  return (string)‪$this->element;
143  }
144 
145  /*******************************
146  *
147  * build up history
148  *
149  *******************************/
155  public function ‪getDiff(array $changeLog): array
156  {
157  $insertsDeletes = [];
158  $newArr = [];
159  $differences = [];
160  // traverse changelog array
161  foreach ($changeLog as $value) {
162  $field = $value['tablename'] . ':' . $value['recuid'];
163  // inserts / deletes
164  if ((int)$value['actiontype'] !== ‪RecordHistoryStore::ACTION_MODIFY) {
165  if (!isset($insertsDeletes[$field])) {
166  $insertsDeletes[$field] = 0;
167  }
168  ($value['action'] ?? '') === 'insert' ? $insertsDeletes[$field]++ : $insertsDeletes[$field]--;
169  // unset not needed fields
170  if ($insertsDeletes[$field] === 0) {
171  unset($insertsDeletes[$field]);
172  }
173  } elseif (!isset($newArr[$field])) {
174  $newArr[$field] = $value['newRecord'];
175  $differences[$field] = $value['oldRecord'];
176  } else {
177  $differences[$field] = array_merge($differences[$field], $value['oldRecord']);
178  }
179  }
180  // remove entries where there were no changes effectively
181  foreach ($newArr as ‪$record => $value) {
182  foreach ($value as $key => $innerVal) {
183  if ($newArr[‪$record][$key] === $differences[‪$record][$key]) {
184  unset($newArr[‪$record][$key], $differences[‪$record][$key]);
185  }
186  }
187  if (empty($newArr[‪$record]) && empty($differences[‪$record])) {
188  unset($newArr[‪$record], $differences[‪$record]);
189  }
190  }
191  return [
192  'newData' => $newArr,
193  'oldData' => $differences,
194  'insertsDeletes' => $insertsDeletes,
195  ];
196  }
197 
203  protected function ‪getHistoryData(string $table, int ‪$uid, bool $includeSubEntries = null, int ‪$lastHistoryEntry = null): array
204  {
205  $historyDataForRecord = $this->‪getHistoryDataForRecord($table, ‪$uid, ‪$lastHistoryEntry);
206  // get history of tables of this page and merge it into changelog
207  if ($table === 'pages' && $includeSubEntries && $this->‪hasPageAccess('pages', $uid)) {
208  foreach (‪$GLOBALS['TCA'] as $tablename => $value) {
209  // check if there are records on the page
210  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tablename);
211  $queryBuilder->getRestrictions()->removeAll();
212 
213  $result = $queryBuilder
214  ->select('uid')
215  ->from($tablename)
216  ->where(
217  $queryBuilder->expr()->eq(
218  'pid',
219  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
220  )
221  )
222  ->executeQuery();
223  while ($row = $result->fetchAssociative()) {
224  // if there is history data available, merge it into changelog
225  $newChangeLog = $this->‪getHistoryDataForRecord($tablename, $row['uid'], ‪$lastHistoryEntry);
226  if (is_array($newChangeLog) && !empty($newChangeLog)) {
227  foreach ($newChangeLog as $key => $newChangeLogEntry) {
228  $historyDataForRecord[$key] = $newChangeLogEntry;
229  }
230  }
231  }
232  }
233  }
234  usort($historyDataForRecord, static function (array $a, array $b): int {
235  if ($a['tstamp'] < $b['tstamp']) {
236  return 1;
237  }
238  if ($a['tstamp'] > $b['tstamp']) {
239  return -1;
240  }
241  return 0;
242  });
243  return $historyDataForRecord;
244  }
245 
255  public function ‪getHistoryDataForRecord(string $table, int ‪$uid, int ‪$lastHistoryEntry = null): array
256  {
257  if (empty(‪$GLOBALS['TCA'][$table]) || !$this->‪hasTableAccess($table) || !$this->‪hasPageAccess($table, ‪$uid)) {
258  return [];
259  }
260 
261  ‪$uid = $this->‪resolveElement($table, ‪$uid);
262  return $this->‪findEventsForRecord($table, ‪$uid, ($this->maxSteps ?: 0), ‪$lastHistoryEntry);
263  }
264 
265  /*******************************
266  *
267  * Various helper functions
268  *
269  *******************************/
270 
278  protected function ‪resolveElement(string $table, int ‪$uid): int
279  {
280  if (isset(‪$GLOBALS['TCA'][$table])
281  && $workspaceVersion = BackendUtility::getWorkspaceVersionOfRecord($this->‪getBackendUser()->workspace, $table, ‪$uid, 'uid')) {
282  ‪$uid = $workspaceVersion['uid'];
283  }
284  return ‪$uid;
285  }
286 
290  protected function ‪getHistoryEntry(int ‪$lastHistoryEntry): array
291  {
292  $queryBuilder = $this->‪getQueryBuilder();
293  ‪$record = $queryBuilder
294  ->select('uid', 'tablename', 'recuid')
295  ->from('sys_history')
296  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$lastHistoryEntry, ‪Connection::PARAM_INT)))
297  ->executeQuery()
298  ->fetchAssociative();
299 
300  if (empty(‪$record)) {
301  return [];
302  }
303 
304  return ‪$record;
305  }
306 
310  public function ‪getCreationInformationForRecord(string $table, array ‪$record): ?array
311  {
312  $queryBuilder = $this->‪getQueryBuilder();
313  $result = $queryBuilder
314  ->select('*')
315  ->from('sys_history')
316  ->where(
317  $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table)),
318  $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter(‪$record['uid'], ‪Connection::PARAM_INT)),
319  $queryBuilder->expr()->eq('actiontype', $queryBuilder->createNamedParameter(‪RecordHistoryStore::ACTION_ADD, ‪Connection::PARAM_INT))
320  )
321  ->setMaxResults(1)
322  ->executeQuery()
323  ->fetchAssociative();
324  return $result ?: null;
325  }
326 
331  public function ‪getCreationInformationForMultipleRecords(string $table, array $recordIds): array
332  {
333  $queryBuilder = $this->‪getQueryBuilder();
334  return $queryBuilder
335  ->select('*')
336  ->from('sys_history')
337  ->where(
338  $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table)),
339  $queryBuilder->expr()->in('recuid', $queryBuilder->createNamedParameter($recordIds, Connection::PARAM_INT_ARRAY)),
340  $queryBuilder->expr()->eq('actiontype', $queryBuilder->createNamedParameter(‪RecordHistoryStore::ACTION_ADD, ‪Connection::PARAM_INT))
341  )
342  ->executeQuery()
343  ->fetchAllAssociative();
344  }
345 
350  public function ‪findEventsForRecord(string $table, int ‪$uid, int $limit = 0, int $minimumUid = null): array
351  {
352  $backendUser = $this->‪getBackendUser();
353  $queryBuilder = $this->‪getQueryBuilder();
354  $queryBuilder
355  ->select('*')
356  ->from('sys_history')
357  ->where(
358  $queryBuilder->expr()->eq('tablename', $queryBuilder->createNamedParameter($table)),
359  $queryBuilder->expr()->eq('recuid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT))
360  );
361  if ($backendUser->workspace === 0) {
362  $queryBuilder->andWhere(
363  $queryBuilder->expr()->eq('workspace', 0)
364  );
365  } else {
366  $queryBuilder->andWhere(
367  $queryBuilder->expr()->or(
368  $queryBuilder->expr()->eq('workspace', 0),
369  $queryBuilder->expr()->eq('workspace', $queryBuilder->createNamedParameter($backendUser->workspace, ‪Connection::PARAM_INT))
370  )
371  );
372  }
373  if ($limit) {
374  $queryBuilder->setMaxResults($limit);
375  }
376 
377  if ($minimumUid) {
378  $queryBuilder->andWhere($queryBuilder->expr()->gte('uid', $queryBuilder->createNamedParameter($minimumUid, ‪Connection::PARAM_INT)));
379  }
380 
381  return $this->‪prepareEventDataFromQueryBuilder($queryBuilder);
382  }
383 
384  public function ‪findEventsForCorrelation(string $correlationId): array
385  {
386  $queryBuilder = $this->‪getQueryBuilder();
387  $queryBuilder
388  ->select('*')
389  ->from('sys_history')
390  ->where($queryBuilder->expr()->eq('correlation_id', $queryBuilder->createNamedParameter($correlationId)));
391 
392  return $this->‪prepareEventDataFromQueryBuilder($queryBuilder);
393  }
394 
395  protected function ‪prepareEventDataFromQueryBuilder(QueryBuilder $queryBuilder): array
396  {
397  $events = [];
398  $result = $queryBuilder->orderBy('tstamp', 'DESC')->executeQuery();
399  while ($row = $result->fetchAssociative()) {
400  ‪$identifier = (int)$row['uid'];
401  $actionType = (int)$row['actiontype'];
402  if ($actionType === ‪RecordHistoryStore::ACTION_ADD || $actionType === ‪RecordHistoryStore::ACTION_UNDELETE) {
403  $row['action'] = 'insert';
404  }
405  if ($actionType === ‪RecordHistoryStore::ACTION_DELETE) {
406  $row['action'] = 'delete';
407  }
408  if ($row['history_data'] === null) {
409  $events[‪$identifier] = $row;
410  continue;
411  }
412  if (str_starts_with($row['history_data'], 'a')) {
413  // legacy code
414  $row['history_data'] = unserialize($row['history_data'], ['allowed_classes' => false]);
415  } else {
416  $row['history_data'] = json_decode($row['history_data'], true);
417  }
418  if (isset($row['history_data']['newRecord'])) {
419  $row['newRecord'] = $row['history_data']['newRecord'];
420  }
421  if (isset($row['history_data']['oldRecord'])) {
422  $row['oldRecord'] = $row['history_data']['oldRecord'];
423  }
424  $events[‪$identifier] = $row;
425  }
426  krsort($events);
427  return $events;
428  }
429 
436  protected function ‪hasPageAccess($table, ‪$uid): bool
437  {
438  $pageRecord = null;
439  ‪$uid = (int)‪$uid;
440 
441  if ($table === 'pages') {
442  $pageId = ‪$uid;
443  } else {
444  ‪$record = BackendUtility::getRecord($table, ‪$uid, '*', '', false);
445  $pageId = (‪$record['pid'] ?? 0);
446  }
447 
448  if ($pageId === 0 && (‪$GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction'] ?? false)) {
449  return true;
450  }
451 
452  if (!isset($this->pageAccessCache[$pageId])) {
453  $isDeletedPage = false;
454  if (isset(‪$GLOBALS['TCA']['pages']['ctrl']['delete'])) {
455  $deletedField = ‪$GLOBALS['TCA']['pages']['ctrl']['delete'];
456  ‪$fields = 'pid,' . $deletedField;
457  $pageRecord = BackendUtility::getRecord('pages', $pageId, ‪$fields, '', false);
458  $isDeletedPage = (bool)($pageRecord[$deletedField] ?? false);
459  }
460  if ($isDeletedPage) {
461  // The page is deleted, so we fake its uid to be the one of the parent page.
462  // By doing so, the following API will use this id to traverse the rootline
463  // and check whether it is in the users' web mounts.
464  // We check however if the user has (or better had) access to the deleted page itself.
465  // Since the only way we got here is by requesting the history of the parent page
466  // we can be sure this parent page actually exists.
467  $pageRecord['uid'] = $pageRecord['pid'];
468  $this->pageAccessCache[$pageId] = $this->‪getBackendUser()->doesUserHaveAccess($pageRecord, ‪Permission::PAGE_SHOW);
469  } else {
470  $this->pageAccessCache[$pageId] = BackendUtility::readPageAccess(
471  $pageId,
472  $this->‪getBackendUser()->getPagePermsClause(‪Permission::PAGE_SHOW)
473  );
474  }
475  }
476 
477  return $this->pageAccessCache[$pageId] !== false;
478  }
479 
486  protected function ‪sanitizeElementValue(string $value): string
487  {
488  if ($value !== '' && !preg_match('#^[a-z\d_.]+:[\d]+$#i', $value)) {
489  return '';
490  }
491  return $value;
492  }
493 
497  protected function ‪sanitizeRollbackFieldsValue(string $value): string
498  {
499  if ($value !== '' && !preg_match('#^[a-z\d_.]+(:[\d]+(:[a-z\d_.]+)?)?$#i', $value)) {
500  return '';
501  }
502  return $value;
503  }
504 
510  protected function ‪hasTableAccess($table): bool
511  {
512  return $this->‪getBackendUser()->check('tables_select', $table);
513  }
514 
515  protected function ‪getBackendUser(): ‪BackendUserAuthentication
516  {
517  return ‪$GLOBALS['BE_USER'];
518  }
519 
520  protected function ‪getQueryBuilder(): QueryBuilder
521  {
522  return GeneralUtility::makeInstance(ConnectionPool::class)
523  ->getQueryBuilderForTable('sys_history');
524  }
525 
526  protected function ‪updateCurrentElement(): void
527  {
528  if ($this->lastHistoryEntry) {
529  $elementData = $this->‪getHistoryEntry($this->lastHistoryEntry);
530  if (!empty($elementData) && empty($this->element)) {
531  $this->element = $elementData['tablename'] . ':' . $elementData['recuid'];
532  }
533  }
534  }
535 }
‪TYPO3\CMS\Backend\History\RecordHistory\getElementInformation
‪getElementInformation()
Definition: RecordHistory.php:126
‪TYPO3\CMS\Backend\History\RecordHistory\getElementString
‪string getElementString()
Definition: RecordHistory.php:134
‪TYPO3\CMS\Backend\History\RecordHistory\getDiff
‪array getDiff(array $changeLog)
Definition: RecordHistory.php:149
‪TYPO3\CMS\Backend\History\RecordHistory\getChangeLog
‪getChangeLog()
Definition: RecordHistory.php:114
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:50
‪TYPO3\CMS\Backend\History\RecordHistory\$rollbackFields
‪string $rollbackFields
Definition: RecordHistory.php:65
‪TYPO3\CMS\Backend\History\RecordHistory\setShowSubElements
‪setShowSubElements(bool $showSubElements)
Definition: RecordHistory.php:106
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\ACTION_MODIFY
‪const ACTION_MODIFY
Definition: RecordHistoryStore.php:33
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Backend\History\RecordHistory\resolveElement
‪int resolveElement(string $table, int $uid)
Definition: RecordHistory.php:272
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryDataForRecord
‪array getHistoryDataForRecord(string $table, int $uid, int $lastHistoryEntry=null)
Definition: RecordHistory.php:249
‪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\setLastHistoryEntryNumber
‪setLastHistoryEntryNumber(int $lastHistoryEntry)
Definition: RecordHistory.php:82
‪TYPO3\CMS\Backend\History\RecordHistory
Definition: RecordHistory.php:32
‪TYPO3\CMS\Webhooks\Message\$record
‪identifier readonly int readonly array $record
Definition: PageModificationMessage.php:36
‪TYPO3\CMS\Backend\History\RecordHistory\setMaxSteps
‪setMaxSteps(int $maxSteps)
Definition: RecordHistory.php:97
‪TYPO3\CMS\Backend\History\RecordHistory\$showSubElements
‪bool $showSubElements
Definition: RecordHistory.php:43
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:60
‪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:514
‪TYPO3\CMS\Backend\History\RecordHistory\hasPageAccess
‪hasPageAccess($table, $uid)
Definition: RecordHistory.php:430
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryEntry
‪getHistoryEntry(int $lastHistoryEntry)
Definition: RecordHistory.php:284
‪TYPO3\CMS\Backend\History\RecordHistory\sanitizeElementValue
‪sanitizeElementValue(string $value)
Definition: RecordHistory.php:480
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:39
‪TYPO3\CMS\Backend\History\RecordHistory\getHistoryData
‪getHistoryData(string $table, int $uid, bool $includeSubEntries=null, int $lastHistoryEntry=null)
Definition: RecordHistory.php:197
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪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\getCreationInformationForMultipleRecords
‪getCreationInformationForMultipleRecords(string $table, array $recordIds)
Definition: RecordHistory.php:325
‪TYPO3\CMS\Backend\History\RecordHistory\getBackendUser
‪getBackendUser()
Definition: RecordHistory.php:509
‪TYPO3\CMS\Backend\History\RecordHistory\getLastHistoryEntryNumber
‪getLastHistoryEntryNumber()
Definition: RecordHistory.php:88
‪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\Backend\History\RecordHistory\findEventsForRecord
‪findEventsForRecord(string $table, int $uid, int $limit=0, int $minimumUid=null)
Definition: RecordHistory.php:344
‪TYPO3\CMS\Backend\History\RecordHistory\getCreationInformationForRecord
‪getCreationInformationForRecord(string $table, array $record)
Definition: RecordHistory.php:304
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:48
‪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:51
‪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:378
‪TYPO3\CMS\Backend\History\RecordHistory\hasTableAccess
‪hasTableAccess($table)
Definition: RecordHistory.php:504
‪TYPO3\CMS\Backend\History\RecordHistory\sanitizeRollbackFieldsValue
‪sanitizeRollbackFieldsValue(string $value)
Definition: RecordHistory.php:491
‪TYPO3\CMS\Webhooks\Message\$identifier
‪identifier readonly string $identifier
Definition: FileAddedMessage.php:37
‪TYPO3\CMS\Backend\History\RecordHistory\prepareEventDataFromQueryBuilder
‪prepareEventDataFromQueryBuilder(QueryBuilder $queryBuilder)
Definition: RecordHistory.php:389
‪TYPO3\CMS\Backend\History\RecordHistory\updateCurrentElement
‪updateCurrentElement()
Definition: RecordHistory.php:520