‪TYPO3CMS  ‪main
DataHandlerHook.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 Doctrine\DBAL\Exception as DBALException;
19 use Psr\EventDispatcher\EventDispatcherInterface;
20 use Symfony\Component\Messenger\MessageBusInterface;
21 use TYPO3\CMS\Backend\Utility\BackendUtility;
31 use ‪TYPO3\CMS\Core\SysLog\Action\Database as DatabaseAction;
32 use ‪TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
44 
51 {
56  protected array ‪$notificationEmailInfo = [];
57 
61  protected array ‪$remappedIds = [];
62 
63  public function ‪__construct(
64  private readonly MessageBusInterface $messageBus,
65  private readonly ‪WorkspacePublishGate $workspacePublishGate,
66  private readonly EventDispatcherInterface $eventDispatcher,
67  ) {}
68 
69  /****************************
70  ***** Cmdmap Hooks ******
71  ****************************/
77  public function ‪processCmdmap_beforeStart(‪DataHandler $dataHandler)
78  {
79  // Reset notification array
80  $this->notificationEmailInfo = [];
81  // Resolve dependencies of version/workspaces actions:
82  $dataHandler->cmdmap = GeneralUtility::makeInstance(CommandMap::class, $dataHandler->cmdmap, $dataHandler->BE_USER->workspace)->process()->get();
83  }
84 
95  public function ‪processCmdmap($command, $table, $id, $value, &$commandIsProcessed, ‪DataHandler $dataHandler)
96  {
97  // custom command "version"
98  if ($command !== 'version') {
99  return;
100  }
101  $commandIsProcessed = true;
102  $action = (string)$value['action'];
103  $comment = $value['comment'] ?? '';
104  $notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? [];
105  switch ($action) {
106  case 'new':
107  $dataHandler->‪versionizeRecord($table, $id, $value['label']);
108  break;
109  case 'swap':
110  case 'publish':
111  $this->‪version_swap(
112  $table,
113  $id,
114  (int)$value['swapWith'],
115  $dataHandler,
116  $comment,
117  $notificationAlternativeRecipients
118  );
119  break;
120  case 'clearWSID':
121  case 'flush':
122  $dataHandler->‪discard($table, (int)$id);
123  break;
124  case 'setStage':
125  $elementIds = ‪GeneralUtility::intExplode(',', (string)$id, true);
126  foreach ($elementIds as $elementId) {
127  $this->‪version_setStage(
128  $table,
129  $elementId,
130  $value['stageId'],
131  $comment,
132  $dataHandler,
133  $notificationAlternativeRecipients
134  );
135  }
136  break;
137  default:
138  // Do nothing
139  }
140  }
141 
148  public function ‪processCmdmap_afterFinish(‪DataHandler $dataHandler)
149  {
150  $emailNotificationService = GeneralUtility::makeInstance(StageChangeNotification::class);
152  $this->notificationEmailInfo,
153  $emailNotificationService,
154  $dataHandler
155  );
156 
157  // Reset notification array
158  $this->notificationEmailInfo = [];
159  // Reset remapped IDs
160  $this->remappedIds = [];
161 
162  $this->‪flushWorkspaceCacheEntriesByWorkspaceId((int)$dataHandler->BE_USER->workspace);
163  }
164 
165  protected function ‪sendStageChangeNotification(
166  array $accumulatedNotificationInformation,
167  ‪StageChangeNotification $notificationService,
168  ‪DataHandler $dataHandler
169  ): void {
170  foreach ($accumulatedNotificationInformation as $groupedNotificationInformation) {
171  $emails = (array)$groupedNotificationInformation['recipients'];
172  if (empty($emails)) {
173  continue;
174  }
175  $workspaceRec = $groupedNotificationInformation['shared'][0];
176  if (!is_array($workspaceRec)) {
177  continue;
178  }
179  $message = new ‪StageChangeMessage(
180  $workspaceRec,
181  (int)$groupedNotificationInformation['shared'][1],
182  $groupedNotificationInformation['elements'],
183  $groupedNotificationInformation['shared'][2],
184  $emails,
185  $dataHandler->BE_USER->user
186  );
187  $this->messageBus->dispatch($message);
188 
189  if ($dataHandler->enableLogging) {
190  [$elementTable, $elementUid] = reset($groupedNotificationInformation['elements']);
191  $propertyArray = $dataHandler->‪getRecordProperties($elementTable, $elementUid);
192  $pid = $propertyArray['pid'];
193  $dataHandler->‪log($elementTable, $elementUid, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::MESSAGE, 'Notification email for stage change was sent to "{recipients}"', -1, ['recipients' => implode('", "', array_column($emails, 'email'))], $dataHandler->‪eventPid($elementTable, $elementUid, $pid));
194  }
195  }
196  }
197 
207  public function ‪processCmdmap_deleteAction($table, $id, array ‪$record, &$recordWasDeleted, ‪DataHandler $dataHandler)
208  {
209  // only process the hook if it wasn't processed
210  // by someone else before
211  if ($recordWasDeleted) {
212  return;
213  }
214  $recordWasDeleted = true;
215  // For Live version, try if there is a workspace version because if so, rather "delete" that instead
216  // Look, if record is an offline version, then delete directly:
217  if ((int)(‪$record['t3ver_oid'] ?? 0) === 0) {
218  if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) {
219  ‪$record = $wsVersion;
220  $id = ‪$record['uid'];
221  }
222  }
223  $recordVersionState = VersionState::tryFrom(‪$record['t3ver_state'] ?? 0);
224  // Look, if record is an offline version, then delete directly:
225  if ((int)(‪$record['t3ver_oid'] ?? 0) > 0) {
226  if (BackendUtility::isTableWorkspaceEnabled($table)) {
227  // In Live workspace, delete any. In other workspaces there must be match.
228  if ($dataHandler->BE_USER->workspace == 0 || (int)‪$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) {
229  $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
230  // Processing can be skipped if a delete placeholder shall be published
231  // during the current request. Thus it will be deleted later on...
232  $liveRecordVersionState = VersionState::tryFrom($liveRec['t3ver_state'] ?? 0);
233  if ($recordVersionState === VersionState::DELETE_PLACEHOLDER && !empty($liveRec['uid'])
234  && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'])
235  && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
236  && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
237  && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
238  ) {
239  return null;
240  }
241 
242  if (‪$record['t3ver_wsid'] > 0 && $recordVersionState === VersionState::DEFAULT_STATE) {
243  // Change normal versioned record to delete placeholder
244  // Happens when an edited record is deleted
245  GeneralUtility::makeInstance(ConnectionPool::class)
246  ->getConnectionForTable($table)
247  ->update(
248  $table,
249  ['t3ver_state' => VersionState::DELETE_PLACEHOLDER->value],
250  ['uid' => $id]
251  );
252 
253  // Delete localization overlays:
254  $dataHandler->‪deleteL10nOverlayRecords($table, $id);
255  } elseif (‪$record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
256  // Delete those in WS 0 + if their live records state was not "Placeholder".
257  $dataHandler->‪deleteEl($table, $id);
258  } elseif ($recordVersionState === VersionState::NEW_PLACEHOLDER) {
259  $placeholderRecord = BackendUtility::getLiveVersionOfRecord($table, (int)$id);
260  $dataHandler->‪deleteEl($table, (int)$id);
261  if (is_array($placeholderRecord)) {
262  $this->‪softOrHardDeleteSingleRecord($table, (int)$placeholderRecord['uid']);
263  }
264  }
265  } else {
266  $dataHandler->‪log($table, (int)$id, DatabaseAction::DELETE, 0, SystemLogErrorClassification::USER_ERROR, 'Tried to delete record from another workspace');
267  }
268  } else {
269  $dataHandler->‪log($table, (int)$id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Versioning not enabled for record with an online ID (t3ver_oid) given');
270  }
271  } elseif ($recordVersionState === VersionState::NEW_PLACEHOLDER) {
272  // If it is a new versioned record, delete it directly.
273  $dataHandler->‪deleteEl($table, $id);
274  } elseif ($dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table)) {
275  // Look, if record is "online" then delete directly.
276  $dataHandler->‪deleteEl($table, $id);
277  } else {
278  // Otherwise, try to delete by versioning:
279  $copyMappingArray = $dataHandler->copyMappingArray;
280  $dataHandler->‪versionizeRecord($table, $id, 'DELETED!', true);
281  // Determine newly created versions:
282  // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
283  $versionizedElements = ArrayUtility::arrayDiffKeyRecursive($dataHandler->copyMappingArray, $copyMappingArray);
284  // Delete localization overlays:
285  foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
286  foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
287  $dataHandler->‪deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
288  }
289  }
290  }
291  }
292 
303  public function ‪processCmdmap_postProcess($command, $table, $id, $value, ‪DataHandler $dataHandler)
304  {
305  if ($command === 'delete') {
306  if ($table === ‪StagesService::TABLE_STAGE) {
307  $this->‪resetStageOfElements((int)$id);
308  } elseif ($table === ‪WorkspaceService::TABLE_WORKSPACE) {
309  $this->‪flushWorkspaceElements((int)$id);
310  $this->‪emitUpdateTopbarSignal();
311  }
312  }
313  }
314 
315  public function ‪processDatamap_afterAllOperations(‪DataHandler $dataHandler): void
316  {
317  if (isset($dataHandler->datamap[‪WorkspaceService::TABLE_WORKSPACE])) {
318  $this->‪emitUpdateTopbarSignal();
319  }
320  }
321 
334  public function ‪moveRecord($table, ‪$uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, ‪DataHandler $dataHandler)
335  {
336  // Only do something in Draft workspace
337  if ($dataHandler->BE_USER->workspace === 0) {
338  return;
339  }
340  $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
341  $recordWasMoved = true;
342  $moveRecVersionState = VersionState::tryFrom($moveRec['t3ver_state'] ?? 0);
343  // Get workspace version of the source record, if any:
344  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, ‪$uid, 'uid,t3ver_oid');
345  if ($tableSupportsVersioning) {
346  // Create version of record first, if it does not exist
347  if (empty($versionedRecord['uid'])) {
348  $dataHandler->‪versionizeRecord($table, ‪$uid, 'MovePointer');
349  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, ‪$uid, 'uid,t3ver_oid');
350  if ((int)$resolvedPid !== (int)$propArr['pid']) {
351  $this->‪moveRecord_processFields($dataHandler, $resolvedPid, $table, ‪$uid);
352  }
353  } elseif ($dataHandler->‪isRecordCopied($table, ‪$uid) && (int)$dataHandler->copyMappingArray[$table][‪$uid] === (int)$versionedRecord['uid']) {
354  // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
355  if ((int)$resolvedPid !== (int)$propArr['pid']) {
356  $this->‪moveRecord_processFields($dataHandler, $resolvedPid, $table, ‪$uid);
357  }
358  }
359  }
360  // Check workspace permissions:
361  $workspaceAccessBlocked = [];
362  // Element was in "New/Deleted/Moved" so it can be moved...
363  $recIsNewVersion = $moveRecVersionState === VersionState::NEW_PLACEHOLDER || $moveRecVersionState->indicatesPlaceholder();
364  $recordMustNotBeVersionized = $dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table);
365  $canMoveRecord = $recIsNewVersion || $tableSupportsVersioning;
366  // Workspace source check:
367  if (!$recIsNewVersion) {
368  $errorCode = $dataHandler->‪workspaceCannotEditRecord($table, $versionedRecord['uid'] ?: ‪$uid);
369  if ($errorCode) {
370  $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
371  } elseif (!$canMoveRecord && !$recordMustNotBeVersionized) {
372  $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
373  }
374  }
375  // Workspace destination check:
376  // All records can be inserted if $recordMustNotBeVersionized is true.
377  // Only new versions can be inserted if $recordMustNotBeVersionized is FALSE.
378  if (!($recordMustNotBeVersionized || $canMoveRecord)) {
379  $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
380  }
381 
382  if (empty($workspaceAccessBlocked)) {
383  $versionedRecordUid = (int)$versionedRecord['uid'];
384  // custom moving not needed, just behave like in live workspace (also for newly versioned records)
385  if (!$versionedRecordUid || !$tableSupportsVersioning || $recIsNewVersion) {
386  $recordWasMoved = false;
387  } else {
388  // If the move operation is done on a versioned record, which is
389  // NOT new/deleted placeholder, then mark the versioned record as "moved"
390  $this->‪moveRecord_moveVersionedRecord($table, (int)‪$uid, (int)$destPid, $versionedRecordUid, $dataHandler);
391  }
392  } else {
393  $dataHandler->‪log($table, $versionedRecord['uid'] ?: ‪$uid, DatabaseAction::MOVE, 0, SystemLogErrorClassification::USER_ERROR, 'Move attempt failed due to workspace restrictions: {reason}', -1, ['reason' => implode(' // ', $workspaceAccessBlocked)]);
394  }
395  }
396 
405  protected function ‪moveRecord_processFields(‪DataHandler $dataHandler, $resolvedPageId, $table, ‪$uid)
406  {
407  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, ‪$uid);
408  if (empty($versionedRecord)) {
409  return;
410  }
411  foreach ($versionedRecord as $field => $value) {
412  if (empty(‪$GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
413  continue;
414  }
416  $dataHandler,
417  $resolvedPageId,
418  $table,
419  ‪$uid,
420  $value,
421  ‪$GLOBALS['TCA'][$table]['columns'][$field]['config']
422  );
423  }
424  }
425 
436  protected function ‪moveRecord_processFieldValue(‪DataHandler $dataHandler, $resolvedPageId, $table, ‪$uid, $value, array $configuration): void
437  {
438  if (($configuration['behaviour']['disableMovingChildrenWithParent'] ?? false)
439  || !in_array($dataHandler->‪getRelationFieldType($configuration), ['list', 'field'], true)
440  || !BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table'])
441  ) {
442  return;
443  }
444 
445  if ($table === 'pages') {
446  // If the inline elements are related to a page record,
447  // make sure they reside at that page and not at its parent
448  $resolvedPageId = ‪$uid;
449  }
450 
451  $dbAnalysis = $this->‪createRelationHandlerInstance();
452  $dbAnalysis->start($value, $configuration['foreign_table'], '', ‪$uid, $table, $configuration);
453 
454  // Moving records to a positive destination will insert each
455  // record at the beginning, thus the order is reversed here:
456  foreach ($dbAnalysis->itemArray as $item) {
457  $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
458  if (empty($versionedRecord)) {
459  continue;
460  }
461  $versionState = VersionState::tryFrom($versionedRecord['t3ver_state'] ?? 0);
462  if ($versionState->indicatesPlaceholder()) {
463  continue;
464  }
465  $dataHandler->‪moveRecord($item['table'], $item['id'], $resolvedPageId);
466  }
467  }
468 
469  /****************************
470  ***** Stage Changes ******
471  ****************************/
482  protected function ‪version_setStage($table, $id, $stageId, string $comment, ‪DataHandler $dataHandler, array $notificationAlternativeRecipients = [])
483  {
484  if (!BackendUtility::isTableWorkspaceEnabled($table)) {
485  $dataHandler->‪log($table, $id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to set stage for record failed: Table "{table}" does not support versioning', -1, ['table' => $table]);
486  return;
487  }
488 
489  ‪$record = BackendUtility::getRecord($table, $id);
490  if (!is_array(‪$record)) {
491  $dataHandler->‪log($table, $id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to set stage for record failed: No Record');
492  } elseif ($errorCode = $dataHandler->‪workspaceCannotEditOfflineVersion($table, ‪$record)) {
493  $dataHandler->‪log($table, $id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to set stage for record failed: {reason}', -1, ['reason' => $errorCode]);
494  } elseif ($dataHandler->‪checkRecordUpdateAccess($table, $id)) {
495  $workspaceInfo = $dataHandler->BE_USER->checkWorkspace(‪$record['t3ver_wsid']);
496  $workspaceId = (int)$workspaceInfo['uid'];
497  $currentStage = (int)‪$record['t3ver_stage'];
498  // check if the user is allowed to the current stage, so it's also allowed to send to next stage
499  if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($currentStage)) {
500  // Set stage of record:
501  GeneralUtility::makeInstance(ConnectionPool::class)
502  ->getConnectionForTable($table)
503  ->update(
504  $table,
505  [
506  't3ver_stage' => $stageId,
507  ],
508  ['uid' => (int)$id]
509  );
510 
511  if ($dataHandler->enableLogging) {
512  $propertyArray = $dataHandler->‪getRecordProperties($table, $id);
513  $pid = $propertyArray['pid'];
514  $dataHandler->‪log($table, $id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to {stage}. Comment was: "{comment}"', -1, ['stage' => $stageId, 'comment' => mb_substr($comment, 0, 100)], $dataHandler->‪eventPid($table, $id, $pid));
515  }
516  // Write the stage change to history
517  $historyStore = $this->‪getRecordHistoryStore($workspaceId, $dataHandler->BE_USER);
518  $historyStore->changeStageForRecord($table, (int)$id, ['current' => $currentStage, 'next' => $stageId, 'comment' => $comment]);
519  if ((int)$workspaceInfo['stagechg_notification'] > 0) {
520  $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$workspaceInfo, $stageId, $comment];
521  $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = [$table, $id];
522  $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['recipients'] = $notificationAlternativeRecipients;
523  }
524  } else {
525  $dataHandler->‪log($table, $id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'The member user tried to set a stage value "{stage}" that was not allowed', -1, ['stage' => $stageId]);
526  }
527  } else {
528  $dataHandler->‪log($table, $id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::USER_ERROR, 'Attempt to set stage for record failed because you do not have edit access');
529  }
530  }
531 
532  /*****************************
533  ***** CMD versioning ******
534  *****************************/
535 
547  protected function ‪version_swap(string $table, int $id, int $swapWith, ‪DataHandler $dataHandler, string $comment, array $notificationAlternativeRecipients)
548  {
549  // Check prerequisites before start publishing
550  // Skip records that have been deleted during the current execution
551  if ($dataHandler->‪hasDeletedRecord($table, $id)) {
552  return;
553  }
554 
555  // First, check if we may actually edit the online record
556  if (!$dataHandler->‪checkRecordUpdateAccess($table, $id)) {
557  $dataHandler->‪log(
558  $table,
559  $id,
560  DatabaseAction::PUBLISH,
561  0,
562  SystemLogErrorClassification::USER_ERROR,
563  'Error: You cannot swap versions for record {table}:{uid} you do not have access to edit',
564  -1,
565  ['table' => $table, 'uid' => $id]
566  );
567  return;
568  }
569  // Select the two versions:
570  // Currently live version, contents will be removed.
571  $curVersion = BackendUtility::getRecord($table, $id, '*');
572  // Versioned records which contents will be moved into $curVersion
573  $isNewRecord = VersionState::tryFrom($curVersion['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER;
574  if ($isNewRecord && is_array($curVersion)) {
575  // @todo: This early return is odd. It means version_swap_processFields() and versionPublishManyToManyRelations()
576  // below are not called for new records to be published. This is "fine" for mm since mm tables have no
577  // t3ver_wsid and need no publish as such. For inline relation publishing, this is indirectly resolved by the
578  // processCmdmap_beforeStart() hook, which adds additional commands for child records - a construct we
579  // may want to avoid altogether due to its complexity. It would be easier to follow if publish here would
580  // handle that instead.
581  $this->‪publishNewRecord($table, $curVersion, $dataHandler, $comment, (array)$notificationAlternativeRecipients);
582  return;
583  }
584  $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
585  if (!(is_array($curVersion) && is_array($swapVersion))) {
586  $dataHandler->‪log(
587  $table,
588  $id,
589  DatabaseAction::PUBLISH,
590  0,
591  SystemLogErrorClassification::SYSTEM_ERROR,
592  'Error: Either online or swap version for {table}:{uid}->{offlineUid} could not be selected',
593  -1,
594  ['table' => $table, 'uid' => $id, 'offlineUid' => $swapWith]
595  );
596  return;
597  }
598  $workspaceId = (int)$swapVersion['t3ver_wsid'];
599  $currentStage = (int)$swapVersion['t3ver_stage'];
600  if (!$this->workspacePublishGate->isGranted($dataHandler->BE_USER, $workspaceId)) {
601  $dataHandler->‪log($table, (int)$id, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::USER_ERROR, 'User could not publish records from workspace #{workspace}', -1, ['workspace' => $workspaceId]);
602  return;
603  }
604  $wsAccess = $dataHandler->BE_USER->checkWorkspace($workspaceId);
605  if (!($workspaceId <= 0 || !($wsAccess['publish_access'] & ‪WorkspaceService::PUBLISH_ACCESS_ONLY_IN_PUBLISH_STAGE) || $currentStage === ‪StagesService::STAGE_PUBLISH_ID)) {
606  $dataHandler->‪log($table, (int)$id, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::USER_ERROR, 'Records in workspace #{workspace} can only be published when in "Publish" stage', -1, ['workspace' => $workspaceId]);
607  return;
608  }
609  if (!($dataHandler->‪doesRecordExist($table, $swapWith, ‪Permission::PAGE_SHOW) && $dataHandler->‪checkRecordUpdateAccess($table, $swapWith))) {
610  $dataHandler->‪log($table, $swapWith, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::USER_ERROR, 'You cannot publish a record you do not have edit and show permissions for');
611  return;
612  }
613  // Check if the swapWith record really IS a version of the original!
614  if (!(((int)$swapVersion['t3ver_oid'] > 0 && (int)$curVersion['t3ver_oid'] === 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) {
615  $dataHandler->‪log($table, $swapWith, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'In offline record, either t3ver_oid was not set or the t3ver_oid didn\'t match the id of the online version as it must');
616  return;
617  }
618  $versionState = VersionState::tryFrom($swapVersion['t3ver_state'] ?? 0);
619 
620  // Find fields to keep
621  $keepFields = $this->‪getUniqueFields($table);
622  // Sorting needs to be exchanged for moved records
623  if (!empty(‪$GLOBALS['TCA'][$table]['ctrl']['sortby']) && $versionState !== VersionState::MOVE_POINTER) {
624  $keepFields[] = ‪$GLOBALS['TCA'][$table]['ctrl']['sortby'];
625  }
626  // l10n-fields must be kept otherwise the localization
627  // will be lost during the publishing
628  if (‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] ?? false) {
629  $keepFields[] = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
630  }
631  // Swap "keepfields"
632  foreach ($keepFields as $fN) {
633  $tmp = $swapVersion[$fN];
634  $swapVersion[$fN] = $curVersion[$fN];
635  $curVersion[$fN] = $tmp;
636  }
637  // Preserve states:
638  $t3ver_state = [];
639  $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
640  // Modify offline version to become online:
641  // Set pid for ONLINE (but not for moved records)
642  if ($versionState !== VersionState::MOVE_POINTER) {
643  $swapVersion['pid'] = (int)$curVersion['pid'];
644  }
645  // We clear this because t3ver_oid only make sense for offline versions
646  // and we want to prevent unintentional misuse of this
647  // value for online records.
648  $swapVersion['t3ver_oid'] = 0;
649  // In case of swapping and the offline record has a state
650  // (like 2 or 4 for deleting or move-pointer) we set the
651  // current workspace ID so the record is not deselected.
652  // @todo: It is odd these information are updated in $swapVersion *before* version_swap_processFields
653  // version_swap_processFields() and versionPublishManyToManyRelations() are called. This leads
654  // to the situation that versionPublishManyToManyRelations() needs another argument to transfer
655  // the "from workspace" information which would usually be retrieved by accessing $swapVersion['t3ver_wsid']
656  $swapVersion['t3ver_wsid'] = 0;
657  $swapVersion['t3ver_stage'] = 0;
658  $swapVersion['t3ver_state'] = VersionState::DEFAULT_STATE->value;
659  // Take care of relations in each field (e.g. IRRE):
660  if (is_array(‪$GLOBALS['TCA'][$table]['columns'])) {
661  foreach (‪$GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
662  if (isset($fieldConf['config']) && is_array($fieldConf['config'])) {
663  $this->‪version_swap_processFields($table, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler);
664  }
665  }
666  }
667  $dataHandler->‪versionPublishManyToManyRelations($table, $curVersion, $swapVersion, $workspaceId);
668  unset($swapVersion['uid']);
669  // Modify online version to become offline:
670  unset($curVersion['uid']);
671  // Mark curVersion to contain the oid
672  $curVersion['t3ver_oid'] = (int)$id;
673  $curVersion['t3ver_wsid'] = 0;
674  // Increment lifecycle counter
675  $curVersion['t3ver_stage'] = 0;
676  $curVersion['t3ver_state'] = VersionState::DEFAULT_STATE->value;
677  // Generating proper history data to prepare logging
678  $dataHandler->‪compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
679  $dataHandler->‪compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
680 
681  // Execute swapping:
682  $sqlErrors = [];
683  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
684  try {
685  $connection->update(
686  $table,
687  $swapVersion,
688  ['uid' => (int)$id]
689  );
690  } catch (DBALException $e) {
691  $sqlErrors[] = $e->getPrevious()->getMessage();
692  }
693 
694  if (empty($sqlErrors)) {
695  try {
696  $connection->update(
697  $table,
698  $curVersion,
699  ['uid' => (int)$swapWith]
700  );
701  } catch (DBALException $e) {
702  $sqlErrors[] = $e->getPrevious()->getMessage();
703  }
704  }
705 
706  if (!empty($sqlErrors)) {
707  $dataHandler->‪log($table, $swapWith, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'During Swapping: SQL errors happened: {reason}', -1, ['reason' => implode('; ', $sqlErrors)]);
708  } else {
709  // Update localized elements to use the live l10n_parent now
710  $this->‪updateL10nOverlayRecordsOnPublish($table, $id, $swapWith, $workspaceId, $dataHandler);
711  // Register swapped ids for later remapping:
712  $this->remappedIds[$table][$id] = $swapWith;
713  $this->remappedIds[$table][$swapWith] = $id;
714  if (VersionState::tryFrom($t3ver_state['swapVersion'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
715  // We're publishing a delete placeholder t3ver_state = 2. This means the live record should
716  // be set to deleted. We're currently in some workspace and deal with a live record here. Thus,
717  // we temporarily set backend user workspace to 0 so all operations happen as in live.
718  $currentUserWorkspace = $dataHandler->BE_USER->workspace;
719  $dataHandler->BE_USER->workspace = 0;
720  $dataHandler->‪deleteEl($table, $id, true);
721  $dataHandler->BE_USER->workspace = $currentUserWorkspace;
722  }
723  $this->eventDispatcher->dispatch(new ‪AfterRecordPublishedEvent($table, $id, $workspaceId));
724  $dataHandler->‪log($table, $id, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::MESSAGE, 'Publishing successful for table "{table}" uid {liveId}=>{versionId}', -1, ['table' => $table, 'versionId' => $swapWith, 'liveId' => $id], $dataHandler->‪eventPid($table, $id, $swapVersion['pid']));
725 
726  // Set log entry for live record:
727  $propArr = $dataHandler->‪getRecordPropertiesFromRow($table, $swapVersion);
728  if (($propArr['t3ver_oid'] ?? 0) > 0) {
729  $label = 'Record "{header}" ({table}:{uid}) was updated. (Offline version)';
730  } else {
731  $label = 'Record "{header}" ({table}:{uid}) was updated. (Online version)';
732  }
733  $dataHandler->‪log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, ['header' => $propArr['header'], 'table' => $table, 'uid' => $id], $propArr['event_pid']);
734  $dataHandler->‪setHistory($table, $id);
735  // Set log entry for offline record:
736  $propArr = $dataHandler->‪getRecordPropertiesFromRow($table, $curVersion);
737  if (($propArr['t3ver_oid'] ?? 0) > 0) {
738  $label = 'Record "{header}" ({table}:{uid}) was updated. (Offline version)';
739  } else {
740  $label = 'Record "{header}" ({table}:{uid}) was updated. (Online version)';
741  }
742  $dataHandler->‪log($table, $swapWith, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, ['header' => $propArr['header'], 'table' => $table, 'uid' => $swapWith], $propArr['event_pid']);
743  $dataHandler->‪setHistory($table, $swapWith);
744 
746  $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
747  $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
748  $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id];
749  $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients;
750  if ($dataHandler->enableLogging) {
751  $propArr = $dataHandler->‪getRecordProperties($table, $id);
752  $pid = $propArr['pid'];
753  $dataHandler->‪log($table, $id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->‪eventPid($table, $id, $pid));
754  }
755  // Write the stage change to the history
756  $historyStore = $this->‪getRecordHistoryStore((int)$wsAccess['uid'], $dataHandler->BE_USER);
757  $historyStore->changeStageForRecord($table, (int)$id, ['current' => $currentStage, 'next' => ‪StagesService::STAGE_PUBLISH_EXECUTE_ID, 'comment' => $comment]);
758 
759  // Clear cache:
760  $dataHandler->‪registerRecordIdForPageCacheClearing($table, $id);
761  // If published, delete the record from the database
762  if ($table === 'pages') {
763  // Note on fifth argument false: At this point both $curVersion and $swapVersion page records are
764  // identical in DB. deleteEl() would now usually find all records assigned to our obsolete
765  // page which at the same time belong to our current version page, and would delete them.
766  // To suppress this, false tells deleteEl() to only delete the obsolete page but not its assigned records.
767  $dataHandler->‪deleteEl($table, $swapWith, true, true, false);
768  } else {
769  $dataHandler->‪deleteEl($table, $swapWith, true, true);
770  }
771 
772  // Update reference index of the live record - which could have been a workspace record in case 'new'
773  $dataHandler->‪updateRefIndex($table, $id, 0);
774  // The 'swapWith' record has been deleted, so we can drop any reference index the record is involved in
775  $dataHandler->‪registerReferenceIndexRowsForDrop($table, $swapWith, (int)$dataHandler->BE_USER->workspace);
776  }
777  }
778 
793  protected function ‪updateL10nOverlayRecordsOnPublish(string $table, int $liveId, int $previouslyUsedVersionId, int $workspaceId, ‪DataHandler $dataHandler): void
794  {
795  if (!BackendUtility::isTableLocalizable($table)) {
796  return;
797  }
798  if (!BackendUtility::isTableWorkspaceEnabled($table)) {
799  return;
800  }
801  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
802  $queryBuilder = $connection->createQueryBuilder();
803  $queryBuilder->getRestrictions()->removeAll();
804 
805  $l10nParentFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
806  $constraints = $queryBuilder->expr()->eq(
807  $l10nParentFieldName,
808  $queryBuilder->createNamedParameter($previouslyUsedVersionId, ‪Connection::PARAM_INT)
809  );
810  $translationSourceFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null;
811  if ($translationSourceFieldName) {
812  $constraints = $queryBuilder->expr()->or(
813  $constraints,
814  $queryBuilder->expr()->eq(
815  $translationSourceFieldName,
816  $queryBuilder->createNamedParameter($previouslyUsedVersionId, ‪Connection::PARAM_INT)
817  )
818  );
819  }
820 
821  $queryBuilder
822  ->select('uid', $l10nParentFieldName)
823  ->from($table)
824  ->where(
825  $constraints,
826  $queryBuilder->expr()->eq(
827  't3ver_wsid',
828  $queryBuilder->createNamedParameter($workspaceId, ‪Connection::PARAM_INT)
829  )
830  );
831 
832  if ($translationSourceFieldName) {
833  $queryBuilder->addSelect($translationSourceFieldName);
834  }
835 
836  $statement = $queryBuilder->executeQuery();
837  while (‪$record = $statement->fetchAssociative()) {
838  $updateFields = [];
839  $dataTypes = [‪Connection::PARAM_INT];
840  if ((int)‪$record[$l10nParentFieldName] === $previouslyUsedVersionId) {
841  $updateFields[$l10nParentFieldName] = $liveId;
842  $dataTypes[] = ‪Connection::PARAM_INT;
843  }
844  if ($translationSourceFieldName && (int)‪$record[$translationSourceFieldName] === $previouslyUsedVersionId) {
845  $updateFields[$translationSourceFieldName] = $liveId;
846  $dataTypes[] = ‪Connection::PARAM_INT;
847  }
848 
849  if (empty($updateFields)) {
850  continue;
851  }
852 
853  $connection->update(
854  $table,
855  $updateFields,
856  ['uid' => (int)‪$record['uid']],
857  $dataTypes
858  );
859  $dataHandler->‪updateRefIndex($table, ‪$record['uid']);
860  }
861  }
862 
873  protected function ‪version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, ‪DataHandler $dataHandler)
874  {
875  if ($dataHandler->‪getRelationFieldType($configuration) !== 'field') {
876  return;
877  }
878  $foreignTable = $configuration['foreign_table'];
879  // Read relations that point to the current record (e.g. live record):
880  $liveRelations = $this->‪createRelationHandlerInstance();
881  $liveRelations->setWorkspaceId(0);
882  $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration);
883  // Read relations that point to the record to be swapped with e.g. draft record):
884  $versionRelations = $this->‪createRelationHandlerInstance();
885  $versionRelations->setUseLiveReferenceIds(false);
886  $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration);
887  // Update relations for both (workspace/versioning) sites:
888  if (!empty($liveRelations->itemArray)) {
889  $dataHandler->‪addRemapAction(
890  $tableName,
891  (int)$liveData['uid'],
892  [$this, 'updateInlineForeignFieldSorting'],
893  [(int)$liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace]
894  );
895  }
896  if (!empty($versionRelations->itemArray)) {
897  $dataHandler->‪addRemapAction(
898  $tableName,
899  (int)$liveData['uid'],
900  [$this, 'updateInlineForeignFieldSorting'],
901  [(int)$liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0]
902  );
903  }
904  }
905 
910  protected function ‪publishNewRecord(string $table, array $newRecordInWorkspace, ‪DataHandler $dataHandler, string $comment, array $notificationAlternativeRecipients): void
911  {
912  $id = (int)$newRecordInWorkspace['uid'];
913  $workspaceId = (int)$newRecordInWorkspace['t3ver_wsid'];
914  if (!$this->workspacePublishGate->isGranted($dataHandler->BE_USER, $workspaceId)) {
915  $dataHandler->‪log($table, $id, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::USER_ERROR, 'User could not publish records from workspace #{workspace}', -1, ['workspace' => $workspaceId]);
916  return;
917  }
918  $wsAccess = $dataHandler->BE_USER->checkWorkspace($workspaceId);
919  if (!($workspaceId <= 0 || !($wsAccess['publish_access'] & ‪WorkspaceService::PUBLISH_ACCESS_ONLY_IN_PUBLISH_STAGE) || (int)$newRecordInWorkspace['t3ver_stage'] === ‪StagesService::STAGE_PUBLISH_ID)) {
920  $dataHandler->‪log($table, $id, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::USER_ERROR, 'Records in workspace #{workspace} can only be published when in "Publish" stage', -1, ['workspace' => $workspaceId]);
921  return;
922  }
923  if (!($dataHandler->‪doesRecordExist($table, $id, ‪Permission::PAGE_SHOW) && $dataHandler->‪checkRecordUpdateAccess($table, $id))) {
924  $dataHandler->‪log($table, $id, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::USER_ERROR, 'You cannot publish a record you do not have edit and show permissions for');
925  return;
926  }
927 
928  // Modify versioned record to become online
929  $updatedFields = [
930  't3ver_oid' => 0,
931  't3ver_wsid' => 0,
932  't3ver_stage' => 0,
933  't3ver_state' => VersionState::DEFAULT_STATE->value,
934  ];
935 
936  try {
937  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
938  $connection->update(
939  $table,
940  $updatedFields,
941  [
942  'uid' => (int)$id,
943  ],
944  [
950  ]
951  );
952  } catch (DBALException $e) {
953  $dataHandler->‪log($table, $id, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::SYSTEM_ERROR, 'During Publishing: SQL errors happened: {reason}', -1, ['reason' => $e->getPrevious()->getMessage()]);
954  }
955 
956  if ($dataHandler->enableLogging) {
957  $dataHandler->‪log($table, $id, DatabaseAction::PUBLISH, 0, SystemLogErrorClassification::MESSAGE, 'Publishing successful for table "{table}" uid {uid} (new record)', -1, ['table' => $table, 'uid' => $id], $dataHandler->‪eventPid($table, $id, $newRecordInWorkspace['pid']));
958  }
959  $this->eventDispatcher->dispatch(new ‪AfterRecordPublishedEvent($table, $id, $workspaceId));
960 
961  // Set log entry for record
962  $propArr = $dataHandler->‪getRecordPropertiesFromRow($table, $newRecordInWorkspace);
963  $dataHandler->‪log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, 'Record "{table}" ({uid}) was updated. (Online version)', 10, ['table' => $propArr['header'], 'uid' => $table . ':' . $id], $propArr['event_pid']);
964  $dataHandler->‪setHistory($table, $id);
965 
967  $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
968  $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment];
969  $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id];
970  $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients;
971  $dataHandler->‪log($table, $id, DatabaseAction::VERSIONIZE, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to {stage}. Comment was: "{comment}"', -1, ['stage' => $stageId, 'comment' => substr($comment, 0, 100)], $dataHandler->‪eventPid($table, $id, $newRecordInWorkspace['pid']));
972  // Write the stage change to the history (usually this is done in updateDB in DataHandler, but we do a manual SQL change)
973  $historyStore = $this->‪getRecordHistoryStore((int)$wsAccess['uid'], $dataHandler->BE_USER);
974  $historyStore->changeStageForRecord($table, $id, ['current' => (int)$newRecordInWorkspace['t3ver_stage'], 'next' => ‪StagesService::STAGE_PUBLISH_EXECUTE_ID, 'comment' => $comment]);
975 
976  // Clear cache
977  $dataHandler->‪registerRecordIdForPageCacheClearing($table, $id);
978  // Update the reference index: Drop the references in the workspace, but update them in the live workspace
979  $dataHandler->‪registerReferenceIndexRowsForDrop($table, $id, $workspaceId);
980  $dataHandler->‪updateRefIndex($table, $id, 0);
981  $this->‪updateReferenceIndexForL10nOverlays($table, $id, $workspaceId, $dataHandler);
982 
983  // When dealing with mm relations on local side, existing refindex rows of the new workspace record
984  // need to be re-calculated for the now live record. Scenario ManyToMany Publish createContentAndAddRelation
985  // These calls are similar to what is done in DH->versionPublishManyToManyRelations() and can not be
986  // used from there since publishing new records does not call that method, see @todo in version_swap().
987  $dataHandler->‪registerReferenceIndexUpdateForReferencesToItem($table, $id, $workspaceId, 0);
988  $dataHandler->‪registerReferenceIndexUpdateForReferencesToItem($table, $id, $workspaceId);
989  }
990 
995  protected function ‪updateReferenceIndexForL10nOverlays(string $table, int $newVersionedRecordId, int $workspaceId, ‪DataHandler $dataHandler): void
996  {
997  if (!BackendUtility::isTableLocalizable($table)) {
998  return;
999  }
1000  if (!BackendUtility::isTableWorkspaceEnabled($table)) {
1001  return;
1002  }
1003  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
1004  $queryBuilder = $connection->createQueryBuilder();
1005  $queryBuilder->getRestrictions()->removeAll();
1006 
1007  $l10nParentFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
1008  $constraints = $queryBuilder->expr()->eq(
1009  $l10nParentFieldName,
1010  $queryBuilder->createNamedParameter($newVersionedRecordId, ‪Connection::PARAM_INT)
1011  );
1012  $translationSourceFieldName = ‪$GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null;
1013  if ($translationSourceFieldName) {
1014  $constraints = $queryBuilder->expr()->or(
1015  $constraints,
1016  $queryBuilder->expr()->eq(
1017  $translationSourceFieldName,
1018  $queryBuilder->createNamedParameter($newVersionedRecordId, ‪Connection::PARAM_INT)
1019  )
1020  );
1021  }
1022 
1023  $queryBuilder
1024  ->select('uid', $l10nParentFieldName)
1025  ->from($table)
1026  ->where(
1027  $constraints,
1028  $queryBuilder->expr()->eq(
1029  't3ver_wsid',
1030  $queryBuilder->createNamedParameter($workspaceId, ‪Connection::PARAM_INT)
1031  )
1032  );
1033 
1034  if ($translationSourceFieldName) {
1035  $queryBuilder->addSelect($translationSourceFieldName);
1036  }
1037 
1038  $statement = $queryBuilder->executeQuery();
1039  while (‪$record = $statement->fetchAssociative()) {
1040  $dataHandler->‪updateRefIndex($table, ‪$record['uid']);
1041  }
1042  }
1043 
1060  public function ‪updateInlineForeignFieldSorting(int $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
1061  {
1062  ‪$remappedIds = [];
1063  // Use remapped ids (live id <-> version id)
1064  foreach ($foreignIds as $foreignId) {
1065  if (!empty($this->remappedIds[$foreignTableName][$foreignId])) {
1066  ‪$remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId];
1067  } else {
1068  ‪$remappedIds[] = $foreignId;
1069  }
1070  }
1071 
1072  $relationHandler = $this->‪createRelationHandlerInstance();
1073  $relationHandler->setWorkspaceId($targetWorkspaceId);
1074  $relationHandler->setUseLiveReferenceIds(false);
1075  $relationHandler->start(implode(',', ‪$remappedIds), $foreignTableName);
1076  $relationHandler->processDeletePlaceholder();
1077  $relationHandler->writeForeignField($configuration, $parentId);
1078  }
1079 
1087  protected function ‪resetStageOfElements(int $stageId): void
1088  {
1089  foreach ($this->‪getTcaTables() as $tcaTable) {
1090  if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1091  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1092  ->getQueryBuilderForTable($tcaTable);
1093 
1094  $queryBuilder
1095  ->update($tcaTable)
1096  ->set('t3ver_stage', ‪StagesService::STAGE_EDIT_ID)
1097  ->where(
1098  $queryBuilder->expr()->eq(
1099  't3ver_stage',
1100  $queryBuilder->createNamedParameter($stageId, ‪Connection::PARAM_INT)
1101  ),
1102  $queryBuilder->expr()->gt(
1103  't3ver_wsid',
1104  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
1105  )
1106  )
1107  ->executeStatement();
1108  }
1109  }
1110  }
1111 
1118  protected function ‪flushWorkspaceElements(int $workspaceId): void
1119  {
1120  $command = [];
1121  foreach ($this->‪getTcaTables() as $tcaTable) {
1122  if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) {
1123  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1124  ->getQueryBuilderForTable($tcaTable);
1125  $queryBuilder->getRestrictions()->removeAll();
1126  $result = $queryBuilder
1127  ->select('uid')
1128  ->from($tcaTable)
1129  ->where(
1130  $queryBuilder->expr()->eq(
1131  't3ver_wsid',
1132  $queryBuilder->createNamedParameter($workspaceId, ‪Connection::PARAM_INT)
1133  ),
1134  // t3ver_oid >= 0 basically omits placeholder records here, those would otherwise
1135  // fail to delete later in DH->discard() and would create "can't do that" log entries.
1136  $queryBuilder->expr()->or(
1137  $queryBuilder->expr()->gt(
1138  't3ver_oid',
1139  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
1140  ),
1141  $queryBuilder->expr()->eq(
1142  't3ver_state',
1143  $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER->value, ‪Connection::PARAM_INT)
1144  )
1145  )
1146  )
1147  ->orderBy('uid')
1148  ->executeQuery();
1149 
1150  while (($recordId = $result->fetchOne()) !== false) {
1151  $command[$tcaTable][$recordId]['version']['action'] = 'flush';
1152  }
1153  }
1154  }
1155  if (!empty($command)) {
1156  // Execute the command array via DataHandler to flush all records from this workspace.
1157  // Switch to target workspace temporarily, otherwise DH->discard() do not
1158  // operate on correct workspace if fetching additional records.
1159  $backendUser = ‪$GLOBALS['BE_USER'];
1160  $savedWorkspace = $backendUser->workspace;
1161  $backendUser->workspace = $workspaceId;
1162  $context = GeneralUtility::makeInstance(Context::class);
1163  $savedWorkspaceContext = $context->getAspect('workspace');
1164  $context->setAspect('workspace', new ‪WorkspaceAspect($workspaceId));
1165 
1166  $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
1167  $dataHandler->start([], $command, $backendUser);
1168  $dataHandler->process_cmdmap();
1169 
1170  $backendUser->workspace = $savedWorkspace;
1171  $context->setAspect('workspace', $savedWorkspaceContext);
1172  }
1173  }
1174 
1178  protected function ‪getTcaTables(): array
1179  {
1180  return array_keys(‪$GLOBALS['TCA']);
1181  }
1182 
1188  protected function ‪flushWorkspaceCacheEntriesByWorkspaceId(int $workspaceId): void
1189  {
1190  $workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache');
1191  $workspacesCache->flushByTag((string)$workspaceId);
1192  }
1193 
1194  /*******************************
1195  ***** helper functions ******
1196  *******************************/
1197 
1210  protected function ‪moveRecord_moveVersionedRecord(string $table, int $liveUid, int $destPid, int $versionedRecordUid, ‪DataHandler $dataHandler): void
1211  {
1212  // If a record gets moved after a record that already has a versioned record
1213  // then the versioned record needs to be placed after the existing one
1214  $originalRecordDestinationPid = $destPid;
1215  $movedTargetRecordInWorkspace = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, abs($destPid), 'uid');
1216  if (is_array($movedTargetRecordInWorkspace) && $destPid < 0) {
1217  $destPid = -$movedTargetRecordInWorkspace['uid'];
1218  }
1219  $dataHandler->‪moveRecord_raw($table, $versionedRecordUid, $destPid);
1220 
1221  $versionedRecord = BackendUtility::getRecord($table, $versionedRecordUid, 'uid,t3ver_state');
1222  if (VersionState::tryFrom($versionedRecord['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER) {
1223  // Update the state of this record to a move placeholder. This is allowed if the
1224  // record is a 'changed' (t3ver_state=0) record: Changing a record and moving it
1225  // around later, should switch it from 'changed' to 'moved'. Deleted placeholders
1226  // however are an 'end-state', they should not be switched to a move placeholder.
1227  // Scenario: For a live page that has a localization, the localization is first
1228  // marked as to-delete in workspace, creating a delete placeholder for that
1229  // localization. Later, the page is moved around, moving the localization along
1230  // with the default language record. The localization should then NOT be switched
1231  // from 'to-delete' to 'moved', this would loose the 'to-delete' information.
1232  GeneralUtility::makeInstance(ConnectionPool::class)
1233  ->getConnectionForTable($table)
1234  ->update(
1235  $table,
1236  [
1237  't3ver_state' => VersionState::MOVE_POINTER->value,
1238  ],
1239  [
1240  'uid' => (int)$versionedRecordUid,
1241  ]
1242  );
1243  }
1244 
1245  // Check for the localizations of that element and move them as well
1246  $dataHandler->‪moveL10nOverlayRecords($table, $liveUid, $destPid, $originalRecordDestinationPid);
1247  }
1248 
1249  protected function ‪emitUpdateTopbarSignal(): void
1250  {
1251  BackendUtility::setUpdateSignal('updateTopbar');
1252  }
1253 
1260  protected function ‪getUniqueFields($table): array
1261  {
1262  $listArr = [];
1263  foreach (‪$GLOBALS['TCA'][$table]['columns'] ?? [] as $field => $configArr) {
1264  if ($configArr['config']['type'] === 'input' || $configArr['config']['type'] === 'email') {
1265  $evalCodesArray = ‪GeneralUtility::trimExplode(',', $configArr['config']['eval'] ?? '', true);
1266  if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1267  $listArr[] = $field;
1268  }
1269  }
1270  }
1271  return $listArr;
1272  }
1273 
1279  protected function ‪softOrHardDeleteSingleRecord(string $table, int ‪$uid): void
1280  {
1281  $deleteField = ‪$GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null;
1282  if ($deleteField) {
1283  GeneralUtility::makeInstance(ConnectionPool::class)
1284  ->getConnectionForTable($table)
1285  ->update(
1286  $table,
1287  [$deleteField => 1],
1288  ['uid' => ‪$uid],
1290  );
1291  } else {
1292  GeneralUtility::makeInstance(ConnectionPool::class)
1293  ->getConnectionForTable($table)
1294  ->delete(
1295  $table,
1296  ['uid' => ‪$uid]
1297  );
1298  }
1299  }
1300 
1309  {
1310  return GeneralUtility::makeInstance(
1311  RecordHistoryStore::class,
1313  (int)$user->user['uid'],
1315  ‪$GLOBALS['EXEC_TIME'],
1316  $workspaceId
1317  );
1318  }
1319 
1321  {
1322  return GeneralUtility::makeInstance(RelationHandler::class);
1323  }
1324 }
‪TYPO3\CMS\Core\DataHandling\DataHandler\updateRefIndex
‪updateRefIndex($table, $uid, int $workspace=null)
Definition: DataHandler.php:7675
‪TYPO3\CMS\Core\DataHandling\DataHandler
Definition: DataHandler.php:94
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\moveRecord_moveVersionedRecord
‪moveRecord_moveVersionedRecord(string $table, int $liveUid, int $destPid, int $versionedRecordUid, DataHandler $dataHandler)
Definition: DataHandlerHook.php:1210
‪TYPO3\CMS\Core\DataHandling\DataHandler\checkRecordUpdateAccess
‪bool checkRecordUpdateAccess($table, $id)
Definition: DataHandler.php:6977
‪TYPO3\CMS\Core\DataHandling\DataHandler\moveRecord
‪moveRecord($table, $uid, $destPid)
Definition: DataHandler.php:4302
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore\USER_BACKEND
‪const USER_BACKEND
Definition: RecordHistoryStore.php:39
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteEl
‪deleteEl(string $table, int $uid, bool $noRecordCheck=false, bool $forceHardDelete=false, bool $deleteRecordsOnPage=true)
Definition: DataHandler.php:5057
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\processCmdmap_deleteAction
‪processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler)
Definition: DataHandlerHook.php:207
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Context\WorkspaceAspect
Definition: WorkspaceAspect.php:31
‪TYPO3\CMS\Workspaces\Authorization\WorkspacePublishGate
Definition: WorkspacePublishGate.php:29
‪TYPO3\CMS\Core\DataHandling\DataHandler\registerRecordIdForPageCacheClearing
‪registerRecordIdForPageCacheClearing($table, $uid, $pid=null)
Definition: DataHandler.php:8749
‪TYPO3\CMS\Workspaces\Event\AfterRecordPublishedEvent
Definition: AfterRecordPublishedEvent.php:24
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\version_swap
‪version_swap(string $table, int $id, int $swapWith, DataHandler $dataHandler, string $comment, array $notificationAlternativeRecipients)
Definition: DataHandlerHook.php:547
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook
Definition: DataHandlerHook.php:51
‪TYPO3\CMS\Workspaces\Service\WorkspaceService\TABLE_WORKSPACE
‪const TABLE_WORKSPACE
Definition: WorkspaceService.php:42
‪TYPO3\CMS\Core\DataHandling\DataHandler\compareFieldArrayWithCurrentAndUnset
‪array compareFieldArrayWithCurrentAndUnset($table, $id, $fieldArray)
Definition: DataHandler.php:8121
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\moveRecord_processFields
‪moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid)
Definition: DataHandlerHook.php:405
‪TYPO3\CMS\Core\Database\RelationHandler
Definition: RelationHandler.php:36
‪TYPO3\CMS\Core\Versioning\VersionState
‪VersionState
Definition: VersionState.php:22
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\version_setStage
‪version_setStage($table, $id, $stageId, string $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients=[])
Definition: DataHandlerHook.php:482
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\publishNewRecord
‪publishNewRecord(string $table, array $newRecordInWorkspace, DataHandler $dataHandler, string $comment, array $notificationAlternativeRecipients)
Definition: DataHandlerHook.php:910
‪TYPO3\CMS\Core\DataHandling\DataHandler\setHistory
‪setHistory($table, $id)
Definition: DataHandler.php:7642
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\processDatamap_afterAllOperations
‪processDatamap_afterAllOperations(DataHandler $dataHandler)
Definition: DataHandlerHook.php:315
‪TYPO3\CMS\Core\DataHandling\DataHandler\discard
‪discard(string $table, ?int $uid, array $record=null)
Definition: DataHandler.php:5777
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\createRelationHandlerInstance
‪createRelationHandlerInstance()
Definition: DataHandlerHook.php:1320
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\flushWorkspaceElements
‪flushWorkspaceElements(int $workspaceId)
Definition: DataHandlerHook.php:1118
‪TYPO3\CMS\Workspaces\DataHandler\CommandMap
Definition: CommandMap.php:35
‪TYPO3\CMS\Core\DataHandling\DataHandler\doesRecordExist
‪bool doesRecordExist($table, $id, int $perms)
Definition: DataHandler.php:7097
‪TYPO3\CMS\Core\DataHandling\DataHandler\workspaceCannotEditOfflineVersion
‪string workspaceCannotEditOfflineVersion(string $table, array $record)
Definition: DataHandler.php:9328
‪TYPO3\CMS\Core\DataHandling\DataHandler\isRecordCopied
‪bool isRecordCopied($table, $uid)
Definition: DataHandler.php:8721
‪TYPO3\CMS\Core\DataHandling\DataHandler\registerReferenceIndexUpdateForReferencesToItem
‪registerReferenceIndexUpdateForReferencesToItem(string $table, int $uid, int $workspace, int $targetWorkspace=null)
Definition: DataHandler.php:7705
‪TYPO3\CMS\Core\SysLog\Action\Database
Definition: Database.php:24
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\$notificationEmailInfo
‪array $notificationEmailInfo
Definition: DataHandlerHook.php:56
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\__construct
‪__construct(private readonly MessageBusInterface $messageBus, private readonly WorkspacePublishGate $workspacePublishGate, private readonly EventDispatcherInterface $eventDispatcher,)
Definition: DataHandlerHook.php:63
‪TYPO3\CMS\Workspaces\Service\StagesService\STAGE_PUBLISH_EXECUTE_ID
‪const STAGE_PUBLISH_EXECUTE_ID
Definition: StagesService.php:36
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Workspaces\Notification\StageChangeNotification
Definition: StageChangeNotification.php:43
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\processCmdmap_beforeStart
‪processCmdmap_beforeStart(DataHandler $dataHandler)
Definition: DataHandlerHook.php:77
‪TYPO3\CMS\Core\Type\Bitmask\Permission
Definition: Permission.php:26
‪TYPO3\CMS\Workspaces\Service\StagesService\STAGE_PUBLISH_ID
‪const STAGE_PUBLISH_ID
Definition: StagesService.php:38
‪TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore
Definition: RecordHistoryStore.php:31
‪TYPO3\CMS\Core\DataHandling\DataHandler\hasDeletedRecord
‪bool hasDeletedRecord($tableName, $uid)
Definition: DataHandler.php:9210
‪TYPO3\CMS\Core\DataHandling\DataHandler\moveL10nOverlayRecords
‪moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid)
Definition: DataHandler.php:4606
‪TYPO3\CMS\Core\DataHandling\DataHandler\getRelationFieldType
‪string bool getRelationFieldType($conf)
Definition: DataHandler.php:8566
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\getUniqueFields
‪array getUniqueFields($table)
Definition: DataHandlerHook.php:1260
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\processCmdmap_postProcess
‪processCmdmap_postProcess($command, $table, $id, $value, DataHandler $dataHandler)
Definition: DataHandlerHook.php:303
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\processCmdmap
‪processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler)
Definition: DataHandlerHook.php:95
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication\getOriginalUserIdWhenInSwitchUserMode
‪int null getOriginalUserIdWhenInSwitchUserMode()
Definition: BackendUserAuthentication.php:2003
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\softOrHardDeleteSingleRecord
‪softOrHardDeleteSingleRecord(string $table, int $uid)
Definition: DataHandlerHook.php:1279
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\emitUpdateTopbarSignal
‪emitUpdateTopbarSignal()
Definition: DataHandlerHook.php:1249
‪TYPO3\CMS\Webhooks\Message\$record
‪identifier readonly int readonly array $record
Definition: PageModificationMessage.php:36
‪TYPO3\CMS\Core\DataHandling\DataHandler\versionizeRecord
‪int null versionizeRecord($table, $id, $label, $delete=false)
Definition: DataHandler.php:6128
‪TYPO3\CMS\Core\DataHandling\DataHandler\moveRecord_raw
‪moveRecord_raw($table, $uid, $destPid)
Definition: DataHandler.php:4380
‪TYPO3\CMS\Core\DataHandling\DataHandler\eventPid
‪int eventPid($table, $uid, $pid)
Definition: DataHandler.php:7480
‪TYPO3\CMS\Core\SysLog\Error
Definition: Error.php:24
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\getRecordHistoryStore
‪getRecordHistoryStore(int $workspaceId, BackendUserAuthentication $user)
Definition: DataHandlerHook.php:1308
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\$remappedIds
‪array $remappedIds
Definition: DataHandlerHook.php:61
‪TYPO3\CMS\Core\DataHandling\DataHandler\addRemapAction
‪addRemapAction($table, $id, callable $callback, array $arguments)
Definition: DataHandler.php:6851
‪TYPO3\CMS\Core\Authentication\BackendUserAuthentication
Definition: BackendUserAuthentication.php:61
‪TYPO3\CMS\Core\DataHandling\DataHandler\deleteL10nOverlayRecords
‪deleteL10nOverlayRecords($table, $uid)
Definition: DataHandler.php:5553
‪TYPO3\CMS\Core\Type\Bitmask\Permission\PAGE_SHOW
‪const PAGE_SHOW
Definition: Permission.php:35
‪TYPO3\CMS\Workspaces\Service\WorkspaceService\PUBLISH_ACCESS_ONLY_IN_PUBLISH_STAGE
‪const PUBLISH_ACCESS_ONLY_IN_PUBLISH_STAGE
Definition: WorkspaceService.php:45
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\resetStageOfElements
‪resetStageOfElements(int $stageId)
Definition: DataHandlerHook.php:1087
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\updateInlineForeignFieldSorting
‪updateInlineForeignFieldSorting(int $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId)
Definition: DataHandlerHook.php:1060
‪TYPO3\CMS\Workspaces\Service\WorkspaceService
Definition: WorkspaceService.php:38
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\moveRecord
‪moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler)
Definition: DataHandlerHook.php:334
‪TYPO3\CMS\Workspaces\Service\StagesService\STAGE_EDIT_ID
‪const STAGE_EDIT_ID
Definition: StagesService.php:39
‪TYPO3\CMS\Workspaces\Service\StagesService
Definition: StagesService.php:33
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\flushWorkspaceCacheEntriesByWorkspaceId
‪flushWorkspaceCacheEntriesByWorkspaceId(int $workspaceId)
Definition: DataHandlerHook.php:1188
‪TYPO3\CMS\Workspaces\Messages\StageChangeMessage
Definition: StageChangeMessage.php:26
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\processCmdmap_afterFinish
‪processCmdmap_afterFinish(DataHandler $dataHandler)
Definition: DataHandlerHook.php:148
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\sendStageChangeNotification
‪sendStageChangeNotification(array $accumulatedNotificationInformation, StageChangeNotification $notificationService, DataHandler $dataHandler)
Definition: DataHandlerHook.php:165
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:26
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\updateL10nOverlayRecordsOnPublish
‪updateL10nOverlayRecordsOnPublish(string $table, int $liveId, int $previouslyUsedVersionId, int $workspaceId, DataHandler $dataHandler)
Definition: DataHandlerHook.php:793
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\version_swap_processFields
‪version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler)
Definition: DataHandlerHook.php:873
‪TYPO3\CMS\Core\DataHandling\DataHandler\getRecordProperties
‪array getRecordProperties($table, $id, $noWSOL=false)
Definition: DataHandler.php:7442
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\getTcaTables
‪getTcaTables()
Definition: DataHandlerHook.php:1178
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Workspaces\Service\StagesService\TABLE_STAGE
‪const TABLE_STAGE
Definition: StagesService.php:34
‪TYPO3\CMS\Core\DataHandling\DataHandler\registerReferenceIndexRowsForDrop
‪registerReferenceIndexRowsForDrop(string $table, int $uid, int $workspace)
Definition: DataHandler.php:7694
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\updateReferenceIndexForL10nOverlays
‪updateReferenceIndexForL10nOverlays(string $table, int $newVersionedRecordId, int $workspaceId, DataHandler $dataHandler)
Definition: DataHandlerHook.php:995
‪TYPO3\CMS\Core\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:751
‪TYPO3\CMS\Core\DataHandling\DataHandler\log
‪int log($table, $recuid, $action, $recpid, $error, $details, $details_nr=-1, $data=[], $event_pid=-1, $NEWid='')
Definition: DataHandler.php:9090
‪TYPO3\CMS\Core\DataHandling\DataHandler\getRecordPropertiesFromRow
‪array null getRecordPropertiesFromRow($table, $row)
Definition: DataHandler.php:7459
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:817
‪TYPO3\CMS\Core\DataHandling\DataHandler\versionPublishManyToManyRelations
‪versionPublishManyToManyRelations(string $table, array $liveRecord, array $workspaceRecord, int $fromWorkspace)
Definition: DataHandler.php:6209
‪TYPO3\CMS\Workspaces\Hook
Definition: BackendUtilityHook.php:18
‪TYPO3\CMS\Core\DataHandling\DataHandler\workspaceCannotEditRecord
‪string false workspaceCannotEditRecord($table, $recData)
Definition: DataHandler.php:9349
‪TYPO3\CMS\Workspaces\Hook\DataHandlerHook\moveRecord_processFieldValue
‪moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $value, array $configuration)
Definition: DataHandlerHook.php:436