1<?php 2namespace TYPO3\CMS\Workspaces\Hook; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use Doctrine\DBAL\DBALException; 18use Doctrine\DBAL\Platforms\SQLServerPlatform; 19use TYPO3\CMS\Backend\Utility\BackendUtility; 20use TYPO3\CMS\Core\Cache\CacheManager; 21use TYPO3\CMS\Core\Core\Environment; 22use TYPO3\CMS\Core\Database\Connection; 23use TYPO3\CMS\Core\Database\ConnectionPool; 24use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction; 25use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 26use TYPO3\CMS\Core\Database\ReferenceIndex; 27use TYPO3\CMS\Core\Database\RelationHandler; 28use TYPO3\CMS\Core\DataHandling\DataHandler; 29use TYPO3\CMS\Core\DataHandling\PlaceholderShadowColumnsResolver; 30use TYPO3\CMS\Core\Localization\LanguageService; 31use TYPO3\CMS\Core\Service\MarkerBasedTemplateService; 32use TYPO3\CMS\Core\Type\Bitmask\Permission; 33use TYPO3\CMS\Core\Utility\ArrayUtility; 34use TYPO3\CMS\Core\Utility\GeneralUtility; 35use TYPO3\CMS\Core\Versioning\VersionState; 36use TYPO3\CMS\Workspaces\DataHandler\CommandMap; 37use TYPO3\CMS\Workspaces\Preview\PreviewUriBuilder; 38use TYPO3\CMS\Workspaces\Service\StagesService; 39use TYPO3\CMS\Workspaces\Service\WorkspaceService; 40 41/** 42 * Contains some parts for staging, versioning and workspaces 43 * to interact with the TYPO3 Core Engine 44 * @internal This is a specific hook implementation and is not considered part of the Public TYPO3 API. 45 */ 46class DataHandlerHook 47{ 48 /** 49 * For accumulating information about workspace stages raised 50 * on elements so a single mail is sent as notification. 51 * previously called "accumulateForNotifEmail" in DataHandler 52 * 53 * @var array 54 */ 55 protected $notificationEmailInfo = []; 56 57 /** 58 * Contains remapped IDs. 59 * 60 * @var array 61 */ 62 protected $remappedIds = []; 63 64 /** 65 * @var WorkspaceService 66 */ 67 protected $workspaceService; 68 69 /**************************** 70 ***** Cmdmap Hooks ****** 71 ****************************/ 72 /** 73 * hook that is called before any cmd of the commandmap is executed 74 * 75 * @param DataHandler $dataHandler reference to the main DataHandler object 76 */ 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 = $this->getCommandMap($dataHandler)->process()->get(); 83 } 84 85 /** 86 * hook that is called when no prepared command was found 87 * 88 * @param string $command the command to be executed 89 * @param string $table the table of the record 90 * @param int $id the ID of the record 91 * @param mixed $value the value containing the data 92 * @param bool $commandIsProcessed can be set so that other hooks or 93 * @param DataHandler $dataHandler reference to the main DataHandler object 94 */ 95 public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler) 96 { 97 // custom command "version" 98 if ($command === 'version') { 99 $commandIsProcessed = true; 100 $action = (string)$value['action']; 101 $comment = !empty($value['comment']) ? $value['comment'] : ''; 102 $notificationAlternativeRecipients = isset($value['notificationAlternativeRecipients']) && is_array($value['notificationAlternativeRecipients']) ? $value['notificationAlternativeRecipients'] : []; 103 switch ($action) { 104 case 'new': 105 $dataHandler->versionizeRecord($table, $id, $value['label']); 106 break; 107 case 'swap': 108 $this->version_swap( 109 $table, 110 $id, 111 $value['swapWith'], 112 (bool)$value['swapIntoWS'], 113 $dataHandler, 114 $comment, 115 true, 116 $notificationAlternativeRecipients 117 ); 118 break; 119 case 'clearWSID': 120 $this->version_clearWSID($table, $id, false, $dataHandler); 121 break; 122 case 'flush': 123 $this->version_clearWSID($table, $id, true, $dataHandler); 124 break; 125 case 'setStage': 126 $elementIds = GeneralUtility::trimExplode(',', $id, true); 127 foreach ($elementIds as $elementId) { 128 $this->version_setStage( 129 $table, 130 $elementId, 131 $value['stageId'], 132 $comment, 133 true, 134 $dataHandler, 135 $notificationAlternativeRecipients 136 ); 137 } 138 break; 139 default: 140 // Do nothing 141 } 142 } 143 } 144 145 /** 146 * hook that is called AFTER all commands of the commandmap was 147 * executed 148 * 149 * @param DataHandler $dataHandler reference to the main DataHandler object 150 */ 151 public function processCmdmap_afterFinish(DataHandler $dataHandler) 152 { 153 // Empty accumulation array: 154 foreach ($this->notificationEmailInfo as $notifItem) { 155 $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $dataHandler, $notifItem['alternativeRecipients']); 156 } 157 // Reset notification array 158 $this->notificationEmailInfo = []; 159 // Reset remapped IDs 160 $this->remappedIds = []; 161 162 $this->flushWorkspaceCacheEntriesByWorkspaceId($dataHandler->BE_USER->workspace); 163 } 164 165 /** 166 * hook that is called when an element shall get deleted 167 * 168 * @param string $table the table of the record 169 * @param int $id the ID of the record 170 * @param array $record The accordant database record 171 * @param bool $recordWasDeleted can be set so that other hooks or 172 * @param DataHandler $dataHandler reference to the main DataHandler object 173 */ 174 public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler) 175 { 176 // only process the hook if it wasn't processed 177 // by someone else before 178 if ($recordWasDeleted) { 179 return; 180 } 181 $recordWasDeleted = true; 182 // For Live version, try if there is a workspace version because if so, rather "delete" that instead 183 // Look, if record is an offline version, then delete directly: 184 if ($record['pid'] != -1) { 185 if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) { 186 $record = $wsVersion; 187 $id = $record['uid']; 188 } 189 } 190 $recordVersionState = VersionState::cast($record['t3ver_state']); 191 // Look, if record is an offline version, then delete directly: 192 if ($record['pid'] == -1) { 193 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { 194 // In Live workspace, delete any. In other workspaces there must be match. 195 if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) { 196 $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state'); 197 // Processing can be skipped if a delete placeholder shall be swapped/published 198 // during the current request. Thus it will be deleted later on... 199 $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']); 200 if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid']) 201 && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action']) 202 && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith']) 203 && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap' 204 && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id 205 ) { 206 return null; 207 } 208 209 if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) { 210 // Change normal versioned record to delete placeholder 211 // Happens when an edited record is deleted 212 GeneralUtility::makeInstance(ConnectionPool::class) 213 ->getConnectionForTable($table) 214 ->update( 215 $table, 216 [ 217 't3ver_label' => 'DELETED!', 218 't3ver_state' => VersionState::DELETE_PLACEHOLDER, 219 ], 220 ['uid' => $id] 221 ); 222 223 // Delete localization overlays: 224 $dataHandler->deleteL10nOverlayRecords($table, $id); 225 } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) { 226 // Delete those in WS 0 + if their live records state was not "Placeholder". 227 $dataHandler->deleteEl($table, $id); 228 // Delete move-placeholder if current version record is a move-to-pointer 229 if ($recordVersionState->equals(VersionState::MOVE_POINTER)) { 230 $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']); 231 if (!empty($movePlaceholder)) { 232 $dataHandler->deleteEl($table, $movePlaceholder['uid']); 233 } 234 } 235 } else { 236 // If live record was placeholder (new/deleted), rather clear 237 // it from workspace (because it clears both version and placeholder). 238 $this->version_clearWSID($table, $id, false, $dataHandler); 239 } 240 } else { 241 $dataHandler->newlog('Tried to delete record from another workspace', 1); 242 } 243 } else { 244 $dataHandler->newlog('Versioning not enabled for record with PID = -1!', 2); 245 } 246 } elseif ($res = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($record['pid'], $table)) { 247 // Look, if record is "online" or in a versionized branch, then delete directly. 248 if ($res > 0) { 249 $dataHandler->deleteEl($table, $id); 250 } else { 251 $dataHandler->newlog('Stage of root point did not allow for deletion', 1); 252 } 253 } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) { 254 // Placeholders for moving operations are deletable directly. 255 // Get record which its a placeholder for and reset the t3ver_state of that: 256 if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) { 257 // Clear the state flag of the workspace version of the record 258 // Setting placeholder state value for version (so it can know it is currently a new version...) 259 260 GeneralUtility::makeInstance(ConnectionPool::class) 261 ->getConnectionForTable($table) 262 ->update( 263 $table, 264 [ 265 't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE) 266 ], 267 ['uid' => (int)$wsRec['uid']] 268 ); 269 } 270 $dataHandler->deleteEl($table, $id); 271 } else { 272 // Otherwise, try to delete by versioning: 273 $copyMappingArray = $dataHandler->copyMappingArray; 274 $dataHandler->versionizeRecord($table, $id, 'DELETED!', true); 275 // Determine newly created versions: 276 // (remove placeholders are copied and modified, thus they appear in the copyMappingArray) 277 $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray); 278 // Delete localization overlays: 279 foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) { 280 foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) { 281 $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId); 282 } 283 } 284 } 285 } 286 287 /** 288 * In case a sys_workspace_stage record is deleted we do a hard reset 289 * for all existing records in that stage to avoid that any of these end up 290 * as orphan records. 291 * 292 * @param string $command 293 * @param string $table 294 * @param string $id 295 * @param string $value 296 * @param DataHandler $dataHandler 297 */ 298 public function processCmdmap_postProcess($command, $table, $id, $value, DataHandler $dataHandler) 299 { 300 if ($command === 'delete') { 301 if ($table === StagesService::TABLE_STAGE) { 302 $this->resetStageOfElements($id); 303 } elseif ($table === WorkspaceService::TABLE_WORKSPACE) { 304 $this->flushWorkspaceElements($id); 305 } 306 } 307 } 308 309 /** 310 * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about 311 * moving records that are *not* in the live workspace 312 * 313 * @param string $table the table of the record 314 * @param int $uid the ID of the record 315 * @param int $destPid Position to move to: $destPid: >=0 then it points to 316 * @param array $propArr Record properties, like header and pid (includes workspace overlay) 317 * @param array $moveRec Record properties, like header and pid (without workspace overlay) 318 * @param int $resolvedPid The final page ID of the record 319 * @param bool $recordWasMoved can be set so that other hooks or 320 * @param DataHandler $dataHandler 321 */ 322 public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler) 323 { 324 // Only do something in Draft workspace 325 if ($dataHandler->BE_USER->workspace === 0) { 326 return; 327 } 328 if ($destPid < 0) { 329 // Fetch move placeholder, since it might point to a new page in the current workspace 330 $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid'); 331 if ($movePlaceHolder !== false) { 332 $resolvedPid = $movePlaceHolder['pid']; 333 } 334 } 335 $recordWasMoved = true; 336 $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']); 337 // Get workspace version of the source record, if any: 338 $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid'); 339 // Handle move-placeholders if the current record is not one already 340 if ( 341 BackendUtility::isTableWorkspaceEnabled($table) 342 && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER) 343 ) { 344 // Create version of record first, if it does not exist 345 if (empty($WSversion['uid'])) { 346 $dataHandler->versionizeRecord($table, $uid, 'MovePointer'); 347 $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid'); 348 if ((int)$resolvedPid !== (int)$propArr['pid']) { 349 $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid); 350 } 351 } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) { 352 // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders 353 if ((int)$resolvedPid !== (int)$propArr['pid']) { 354 $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid); 355 } 356 } 357 } 358 // Check workspace permissions: 359 $workspaceAccessBlocked = []; 360 // Element was in "New/Deleted/Moved" so it can be moved... 361 $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder(); 362 $destRes = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table); 363 $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableWorkspaceEnabled($table)); 364 // Workspace source check: 365 if (!$recIsNewVersion) { 366 $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid); 367 if ($errorCode) { 368 $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' '; 369 } elseif (!$canMoveRecord && $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) { 370 $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" '; 371 } 372 } 373 // Workspace destination check: 374 // All records can be inserted if $destRes is greater than zero. 375 // Only new versions can be inserted if $destRes is FALSE. 376 // NO RECORDS can be inserted if $destRes is negative which indicates a stage 377 // not allowed for use. If "versioningWS" is version 2, moving can take place of versions. 378 // since TYPO3 CMS 7, version2 is the default and the only option 379 if (!($destRes > 0 || $canMoveRecord && !$destRes)) { 380 $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" '; 381 } elseif ($destRes == 1 && $WSversion['uid']) { 382 $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID '; 383 } 384 if (empty($workspaceAccessBlocked)) { 385 // If the move operation is done on a versioned record, which is 386 // NOT new/deleted placeholder and versioningWS is in version 2, then... 387 // since TYPO3 CMS 7, version2 is the default and the only option 388 if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) { 389 $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $dataHandler); 390 } else { 391 // moving not needed, just behave like in live workspace 392 $recordWasMoved = false; 393 } 394 } else { 395 $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1); 396 } 397 } 398 399 /** 400 * Processes fields of a moved record and follows references. 401 * 402 * @param DataHandler $dataHandler Calling DataHandler instance 403 * @param int $resolvedPageId Resolved real destination page id 404 * @param string $table Name of parent table 405 * @param int $uid UID of the parent record 406 */ 407 protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid) 408 { 409 $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid); 410 if (empty($versionedRecord)) { 411 return; 412 } 413 foreach ($versionedRecord as $field => $value) { 414 if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) { 415 continue; 416 } 417 $this->moveRecord_processFieldValue( 418 $dataHandler, 419 $resolvedPageId, 420 $table, 421 $uid, 422 $field, 423 $value, 424 $GLOBALS['TCA'][$table]['columns'][$field]['config'] 425 ); 426 } 427 } 428 429 /** 430 * Processes a single field of a moved record and follows references. 431 * 432 * @param DataHandler $dataHandler Calling DataHandler instance 433 * @param int $resolvedPageId Resolved real destination page id 434 * @param string $table Name of parent table 435 * @param int $uid UID of the parent record 436 * @param string $field Name of the field of the parent record 437 * @param string $value Value of the field of the parent record 438 * @param array $configuration TCA field configuration of the parent record 439 */ 440 protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration) 441 { 442 $inlineFieldType = $dataHandler->getInlineFieldType($configuration); 443 $inlineProcessing = ( 444 ($inlineFieldType === 'list' || $inlineFieldType === 'field') 445 && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table']) 446 && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent']) 447 ); 448 449 if ($inlineProcessing) { 450 if ($table === 'pages') { 451 // If the inline elements are related to a page record, 452 // make sure they reside at that page and not at its parent 453 $resolvedPageId = $uid; 454 } 455 456 $dbAnalysis = $this->createRelationHandlerInstance(); 457 $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration); 458 459 // Moving records to a positive destination will insert each 460 // record at the beginning, thus the order is reversed here: 461 foreach ($dbAnalysis->itemArray as $item) { 462 $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state'); 463 if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) { 464 continue; 465 } 466 $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId); 467 } 468 } 469 } 470 471 /**************************** 472 ***** Notifications ****** 473 ****************************/ 474 /** 475 * Send an email notification to users in workspace 476 * 477 * @param array $stat Workspace access array from \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::checkWorkspace() 478 * @param int $stageId New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected! 479 * @param string $table Table name of element (or list of element names if $id is zero) 480 * @param int $id Record uid of element (if zero, then $table is used as reference to element(s) alone) 481 * @param string $comment User comment sent along with action 482 * @param DataHandler $dataHandler DataHandler object 483 * @param array $notificationAlternativeRecipients List of recipients to notify instead of be_users selected by sys_workspace, list is generated by workspace extension module 484 */ 485 protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = []) 486 { 487 $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']); 488 // So, if $id is not set, then $table is taken to be the complete element name! 489 $elementName = $id ? $table . ':' . $id : $table; 490 if (!is_array($workspaceRec)) { 491 return; 492 } 493 494 // Get the new stage title 495 $stageService = GeneralUtility::makeInstance(StagesService::class); 496 $newStage = $stageService->getStageTitle((int)$stageId); 497 if (empty($notificationAlternativeRecipients)) { 498 // Compile list of recipients: 499 $emails = []; 500 switch ((int)$stat['stagechg_notification']) { 501 case 1: 502 switch ((int)$stageId) { 503 case 1: 504 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']); 505 break; 506 case 10: 507 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); 508 break; 509 case -1: 510 // List of elements to reject: 511 $allElements = explode(',', $elementName); 512 // Traverse them, and find the history of each 513 foreach ($allElements as $elRef) { 514 [$eTable, $eUid] = explode(':', $elRef); 515 516 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 517 ->getQueryBuilderForTable('sys_log'); 518 519 $queryBuilder->getRestrictions()->removeAll(); 520 521 $result = $queryBuilder 522 ->select('log_data', 'tstamp', 'userid') 523 ->from('sys_log') 524 ->where( 525 $queryBuilder->expr()->eq( 526 'action', 527 $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT) 528 ), 529 $queryBuilder->expr()->eq( 530 'details_nr', 531 $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT) 532 ), 533 $queryBuilder->expr()->eq( 534 'tablename', 535 $queryBuilder->createNamedParameter($eTable, \PDO::PARAM_STR) 536 ), 537 $queryBuilder->expr()->eq( 538 'recuid', 539 $queryBuilder->createNamedParameter($eUid, \PDO::PARAM_INT) 540 ) 541 ) 542 ->orderBy('uid', 'DESC') 543 ->execute(); 544 545 // Find all implicated since the last stage-raise from editing to review: 546 while ($dat = $result->fetch()) { 547 $data = unserialize($dat['log_data']); 548 $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails; 549 if ($data['stage'] == 1) { 550 break; 551 } 552 } 553 } 554 break; 555 case 0: 556 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']); 557 break; 558 default: 559 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); 560 } 561 break; 562 case 10: 563 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); 564 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails; 565 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails; 566 break; 567 default: 568 // Do nothing 569 } 570 } else { 571 $emails = $notificationAlternativeRecipients; 572 } 573 // prepare and then send the emails 574 if (!empty($emails)) { 575 $previewUriBuilder = GeneralUtility::makeInstance(PreviewUriBuilder::class); 576 // Path to record is found: 577 [$elementTable, $elementUid] = explode(':', $elementName); 578 $elementUid = (int)$elementUid; 579 $elementRecord = BackendUtility::getRecord($elementTable, $elementUid); 580 $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord); 581 if ($elementTable === 'pages') { 582 $pageUid = $elementUid; 583 } else { 584 BackendUtility::fixVersioningPid($elementTable, $elementRecord); 585 $pageUid = ($elementUid = $elementRecord['pid']); 586 } 587 588 // new way, options are 589 // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject 590 // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject 591 $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid); 592 $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.']; 593 $markers = [ 594 '###RECORD_TITLE###' => $recordTitle, 595 '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20), 596 '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], 597 '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir, 598 '###WORKSPACE_TITLE###' => $workspaceRec['title'], 599 '###WORKSPACE_UID###' => $workspaceRec['uid'], 600 '###ELEMENT_NAME###' => $elementName, 601 '###NEXT_STAGE###' => $newStage, 602 '###COMMENT###' => $comment, 603 // See: #30212 - keep both markers for compatibility 604 '###USER_REALNAME###' => $dataHandler->BE_USER->user['realName'], 605 '###USER_FULLNAME###' => $dataHandler->BE_USER->user['realName'], 606 '###USER_USERNAME###' => $dataHandler->BE_USER->user['username'] 607 ]; 608 // add marker for preview links if workspace extension is loaded 609 $this->workspaceService = GeneralUtility::makeInstance(WorkspaceService::class); 610 // only generate the link if the marker is in the template - prevents database from getting to much entries 611 if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) { 612 $tempEmailMessage = $this->getLanguageService()->sL($emailConfig['message']); 613 } else { 614 $tempEmailMessage = $emailConfig['message']; 615 } 616 if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) { 617 $markers['###PREVIEW_LINK###'] = $previewUriBuilder->buildUriForPage((int)$elementUid, 0); 618 } 619 unset($tempEmailMessage); 620 621 $markers['###SPLITTED_PREVIEW_LINK###'] = $previewUriBuilder->buildUriForWorkspaceSplitPreview((int)$elementUid, true); 622 // Hook for preprocessing of the content for formmails: 623 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] ?? [] as $className) { 624 $_procObj = GeneralUtility::makeInstance($className); 625 $markers = $_procObj->postModifyMarkers($markers, $this); 626 } 627 // send an email to each individual user, to ensure the 628 // multilanguage version of the email 629 $emailRecipients = []; 630 // an array of language objects that are needed 631 // for emails with different languages 632 $languageObjects = [ 633 $this->getLanguageService()->lang => $this->getLanguageService() 634 ]; 635 // loop through each recipient and send the email 636 foreach ($emails as $recipientData) { 637 // don't send an email twice 638 if (isset($emailRecipients[$recipientData['email']])) { 639 continue; 640 } 641 $emailSubject = $emailConfig['subject']; 642 $emailMessage = $emailConfig['message']; 643 $emailRecipients[$recipientData['email']] = $recipientData['email']; 644 // check if the email needs to be localized 645 // in the users' language 646 if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) { 647 $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default'; 648 if (!isset($languageObjects[$recipientLanguage])) { 649 // a LANG object in this language hasn't been 650 // instantiated yet, so this is done here 651 $languageObject = GeneralUtility::makeInstance(LanguageService::class); 652 $languageObject->init($recipientLanguage); 653 $languageObjects[$recipientLanguage] = $languageObject; 654 } else { 655 $languageObject = $languageObjects[$recipientLanguage]; 656 } 657 if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) { 658 $emailSubject = $languageObject->sL($emailSubject); 659 } 660 if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) { 661 $emailMessage = $languageObject->sL($emailMessage); 662 } 663 } 664 $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class); 665 $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true); 666 $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true); 667 // Send an email to the recipient 668 /** @var \TYPO3\CMS\Core\Mail\MailMessage $mail */ 669 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class); 670 if (!empty($recipientData['realName'])) { 671 $recipient = [$recipientData['email'] => $recipientData['realName']]; 672 } else { 673 $recipient = $recipientData['email']; 674 } 675 $mail->setTo($recipient) 676 ->setSubject($emailSubject) 677 ->setBody($emailMessage); 678 $mail->send(); 679 } 680 $emailRecipients = implode(',', $emailRecipients); 681 if ($dataHandler->enableLogging) { 682 $propertyArray = $dataHandler->getRecordProperties($table, $id); 683 $pid = $propertyArray['pid']; 684 $dataHandler->log($table, $id, 0, 0, 0, 'Notification email for stage change was sent to "' . $emailRecipients . '"', -1, [], $dataHandler->eventPid($table, $id, $pid)); 685 } 686 } 687 } 688 689 /** 690 * Return be_users that should be notified on stage change from input list. 691 * previously called notifyStageChange_getEmails() in DataHandler 692 * 693 * @param string $listOfUsers List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set. 694 * @param bool $noTablePrefix If set, the input list are integers and not strings. 695 * @return array Array of emails 696 */ 697 protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = false) 698 { 699 $users = GeneralUtility::trimExplode(',', $listOfUsers, true); 700 $emails = []; 701 foreach ($users as $userIdent) { 702 if ($noTablePrefix) { 703 $id = (int)$userIdent; 704 } else { 705 [$table, $id] = GeneralUtility::revExplode('_', $userIdent, 2); 706 } 707 if ($table === 'be_users' || $noTablePrefix) { 708 if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) { 709 if (trim($userRecord['email']) !== '') { 710 $emails[$id] = $userRecord; 711 } 712 } 713 } 714 } 715 return $emails; 716 } 717 718 /**************************** 719 ***** Stage Changes ****** 720 ****************************/ 721 /** 722 * Setting stage of record 723 * 724 * @param string $table Table name 725 * @param int $id 726 * @param int $stageId Stage ID to set 727 * @param string $comment Comment that goes into log 728 * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email? 729 * @param DataHandler $dataHandler DataHandler object 730 * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users 731 */ 732 protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = false, DataHandler $dataHandler, array $notificationAlternativeRecipients = []) 733 { 734 if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) { 735 $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, 1); 736 } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) { 737 $record = BackendUtility::getRecord($table, $id); 738 $stat = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']); 739 // check if the usere is allowed to the current stage, so it's also allowed to send to next stage 740 if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) { 741 // Set stage of record: 742 GeneralUtility::makeInstance(ConnectionPool::class) 743 ->getConnectionForTable($table) 744 ->update( 745 $table, 746 [ 747 't3ver_stage' => $stageId, 748 ], 749 ['uid' => (int)$id] 750 ); 751 752 if ($dataHandler->enableLogging) { 753 $propertyArray = $dataHandler->getRecordProperties($table, $id); 754 $pid = $propertyArray['pid']; 755 $dataHandler->log($table, $id, 0, 0, 0, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid)); 756 } 757 // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere! 758 $dataHandler->log($table, $id, 6, 0, 0, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]); 759 if ((int)$stat['stagechg_notification'] > 0) { 760 if ($notificationEmailInfo) { 761 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment]; 762 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id; 763 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients; 764 } else { 765 $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients); 766 } 767 } 768 } else { 769 $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1); 770 } 771 } else { 772 $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', 1); 773 } 774 } 775 776 /***************************** 777 ***** CMD versioning ****** 778 *****************************/ 779 780 /** 781 * Swapping versions of a record 782 * Version from archive (future/past, called "swap version") will get the uid of the "t3ver_oid", the official element with uid = "t3ver_oid" will get the new versions old uid. PIDs are swapped also 783 * 784 * @param string $table Table name 785 * @param int $id UID of the online record to swap 786 * @param int $swapWith UID of the archived version to swap with! 787 * @param bool $swapIntoWS If set, swaps online into workspace instead of publishing out of workspace. 788 * @param DataHandler $dataHandler DataHandler object 789 * @param string $comment Notification comment 790 * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email? 791 * @param array $notificationAlternativeRecipients comma separated list of recipients to notificate instead of normal be_users 792 */ 793 protected function version_swap($table, $id, $swapWith, $swapIntoWS = false, DataHandler $dataHandler, $comment = '', $notificationEmailInfo = false, $notificationAlternativeRecipients = []) 794 { 795 796 // Check prerequisites before start swapping 797 798 // Skip records that have been deleted during the current execution 799 if ($dataHandler->hasDeletedRecord($table, $id)) { 800 return; 801 } 802 803 // First, check if we may actually edit the online record 804 if (!$dataHandler->checkRecordUpdateAccess($table, $id)) { 805 $dataHandler->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1); 806 return; 807 } 808 // Select the two versions: 809 $curVersion = BackendUtility::getRecord($table, $id, '*'); 810 $swapVersion = BackendUtility::getRecord($table, $swapWith, '*'); 811 $movePlh = []; 812 $movePlhID = 0; 813 if (!(is_array($curVersion) && is_array($swapVersion))) { 814 $dataHandler->newlog('Error: Either online or swap version could not be selected!', 2); 815 return; 816 } 817 if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) { 818 $dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1); 819 return; 820 } 821 $wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']); 822 if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10)) { 823 $dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1); 824 return; 825 } 826 if (!($dataHandler->doesRecordExist($table, $swapWith, 'show') && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) { 827 $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', 1); 828 return; 829 } 830 if ($swapIntoWS && !$dataHandler->BE_USER->workspaceSwapAccess()) { 831 $dataHandler->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1); 832 return; 833 } 834 // Check if the swapWith record really IS a version of the original! 835 if (!(((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) { 836 $dataHandler->newlog('In swap version, either pid was not -1 or the t3ver_oid didn\'t match the id of the online version as it must!', 2); 837 return; 838 } 839 // Lock file name: 840 $lockFileName = Environment::getVarPath() . '/lock/swap' . $table . '_' . $id . '.ser'; 841 if (@is_file($lockFileName)) { 842 $dataHandler->newlog('A swapping lock file was present. Either another swap process is already running or a previous swap process failed. Ask your administrator to handle the situation.', 2); 843 return; 844 } 845 846 // Now start to swap records by first creating the lock file 847 848 // Write lock-file: 849 GeneralUtility::writeFileToTypo3tempDir($lockFileName, serialize([ 850 'tstamp' => $GLOBALS['EXEC_TIME'], 851 'user' => $dataHandler->BE_USER->user['username'], 852 'curVersion' => $curVersion, 853 'swapVersion' => $swapVersion 854 ])); 855 // Find fields to keep 856 $keepFields = $this->getUniqueFields($table); 857 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { 858 $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby']; 859 } 860 // l10n-fields must be kept otherwise the localization 861 // will be lost during the publishing 862 if ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) { 863 $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; 864 } 865 // Swap "keepfields" 866 foreach ($keepFields as $fN) { 867 $tmp = $swapVersion[$fN]; 868 $swapVersion[$fN] = $curVersion[$fN]; 869 $curVersion[$fN] = $tmp; 870 } 871 // Preserve states: 872 $t3ver_state = []; 873 $t3ver_state['swapVersion'] = $swapVersion['t3ver_state']; 874 $t3ver_state['curVersion'] = $curVersion['t3ver_state']; 875 // Modify offline version to become online: 876 $tmp_wsid = $swapVersion['t3ver_wsid']; 877 // Set pid for ONLINE 878 $swapVersion['pid'] = (int)$curVersion['pid']; 879 // We clear this because t3ver_oid only make sense for offline versions 880 // and we want to prevent unintentional misuse of this 881 // value for online records. 882 $swapVersion['t3ver_oid'] = 0; 883 // In case of swapping and the offline record has a state 884 // (like 2 or 4 for deleting or move-pointer) we set the 885 // current workspace ID so the record is not deselected 886 // in the interface by BackendUtility::versioningPlaceholderClause() 887 $swapVersion['t3ver_wsid'] = 0; 888 if ($swapIntoWS) { 889 if ($t3ver_state['swapVersion'] > 0) { 890 $swapVersion['t3ver_wsid'] = $dataHandler->BE_USER->workspace; 891 } else { 892 $swapVersion['t3ver_wsid'] = (int)$curVersion['t3ver_wsid']; 893 } 894 } 895 $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME']; 896 $swapVersion['t3ver_stage'] = 0; 897 if (!$swapIntoWS) { 898 $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE); 899 } 900 // Moving element. 901 if (BackendUtility::isTableWorkspaceEnabled($table)) { 902 // && $t3ver_state['swapVersion']==4 // Maybe we don't need this? 903 if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) { 904 $movePlhID = $plhRec['uid']; 905 $movePlh['pid'] = $swapVersion['pid']; 906 $swapVersion['pid'] = (int)$plhRec['pid']; 907 $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state']; 908 $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE); 909 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { 910 // sortby is a "keepFields" which is why this will work... 911 $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']]; 912 $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']]; 913 } 914 } 915 } 916 // Take care of relations in each field (e.g. IRRE): 917 if (is_array($GLOBALS['TCA'][$table]['columns'])) { 918 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) { 919 if (isset($fieldConf['config']) && is_array($fieldConf['config'])) { 920 $this->version_swap_processFields($table, $field, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler); 921 } 922 } 923 } 924 unset($swapVersion['uid']); 925 // Modify online version to become offline: 926 unset($curVersion['uid']); 927 // Set pid for OFFLINE 928 $curVersion['pid'] = -1; 929 $curVersion['t3ver_oid'] = (int)$id; 930 $curVersion['t3ver_wsid'] = $swapIntoWS ? (int)$tmp_wsid : 0; 931 $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME']; 932 $curVersion['t3ver_count'] = $curVersion['t3ver_count'] + 1; 933 // Increment lifecycle counter 934 $curVersion['t3ver_stage'] = 0; 935 if (!$swapIntoWS) { 936 $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE); 937 } 938 // Registering and swapping MM relations in current and swap records: 939 $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith); 940 // Generating proper history data to prepare logging 941 $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion); 942 $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion); 943 944 // Execute swapping: 945 $sqlErrors = []; 946 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); 947 948 $platform = $connection->getDatabasePlatform(); 949 $tableDetails = null; 950 if ($platform instanceof SQLServerPlatform) { 951 // mssql needs to set proper PARAM_LOB and others to update fields 952 $tableDetails = $connection->getSchemaManager()->listTableDetails($table); 953 } 954 955 try { 956 $types = []; 957 958 if ($platform instanceof SQLServerPlatform) { 959 foreach ($curVersion as $columnName => $columnValue) { 960 $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType(); 961 } 962 } 963 964 $connection->update( 965 $table, 966 $swapVersion, 967 ['uid' => (int)$id], 968 $types 969 ); 970 } catch (DBALException $e) { 971 $sqlErrors[] = $e->getPrevious()->getMessage(); 972 } 973 974 if (empty($sqlErrors)) { 975 try { 976 $types = []; 977 if ($platform instanceof SQLServerPlatform) { 978 foreach ($curVersion as $columnName => $columnValue) { 979 $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType(); 980 } 981 } 982 983 $connection->update( 984 $table, 985 $curVersion, 986 ['uid' => (int)$swapWith], 987 $types 988 ); 989 unlink($lockFileName); 990 } catch (DBALException $e) { 991 $sqlErrors[] = $e->getPrevious()->getMessage(); 992 } 993 } 994 995 if (!empty($sqlErrors)) { 996 $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2); 997 } else { 998 // Register swapped ids for later remapping: 999 $this->remappedIds[$table][$id] = $swapWith; 1000 $this->remappedIds[$table][$swapWith] = $id; 1001 // If a moving operation took place...: 1002 if ($movePlhID) { 1003 // Remove, if normal publishing: 1004 if (!$swapIntoWS) { 1005 // For delete + completely delete! 1006 $dataHandler->deleteEl($table, $movePlhID, true, true); 1007 } else { 1008 // Otherwise update the movePlaceholder: 1009 GeneralUtility::makeInstance(ConnectionPool::class) 1010 ->getConnectionForTable($table) 1011 ->update( 1012 $table, 1013 $movePlh, 1014 ['uid' => (int)$movePlhID] 1015 ); 1016 $dataHandler->addRemapStackRefIndex($table, $movePlhID); 1017 } 1018 } 1019 // Checking for delete: 1020 // Delete only if new/deleted placeholders are there. 1021 if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === 1 || (int)$t3ver_state['swapVersion'] === 2)) { 1022 // Force delete 1023 $dataHandler->deleteEl($table, $id, true); 1024 } 1025 if ($dataHandler->enableLogging) { 1026 $dataHandler->log($table, $id, 0, 0, 0, ($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, -1, [], $dataHandler->eventPid($table, $id, $swapVersion['pid'])); 1027 } 1028 1029 // Update reference index of the live record: 1030 $dataHandler->addRemapStackRefIndex($table, $id); 1031 // Set log entry for live record: 1032 $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion); 1033 if ($propArr['_ORIG_pid'] == -1) { 1034 $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated'); 1035 } else { 1036 $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated'); 1037 } 1038 $theLogId = $dataHandler->log($table, $id, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']); 1039 $dataHandler->setHistory($table, $id, $theLogId); 1040 // Update reference index of the offline record: 1041 $dataHandler->addRemapStackRefIndex($table, $swapWith); 1042 // Set log entry for offline record: 1043 $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion); 1044 if ($propArr['_ORIG_pid'] == -1) { 1045 $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated'); 1046 } else { 1047 $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated'); 1048 } 1049 $theLogId = $dataHandler->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']); 1050 $dataHandler->setHistory($table, $swapWith, $theLogId); 1051 1052 $stageId = -20; // \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_EXECUTE_ID; 1053 if ($notificationEmailInfo) { 1054 $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment; 1055 $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment]; 1056 $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = $table . ':' . $id; 1057 $this->notificationEmailInfo[$notificationEmailInfoKey]['alternativeRecipients'] = $notificationAlternativeRecipients; 1058 } else { 1059 $this->notifyStageChange($wsAccess, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients); 1060 } 1061 // Write to log with stageId -20 1062 if ($dataHandler->enableLogging) { 1063 $propArr = $dataHandler->getRecordProperties($table, $id); 1064 $pid = $propArr['pid']; 1065 $dataHandler->log($table, $id, 0, 0, 0, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid)); 1066 } 1067 $dataHandler->log($table, $id, 6, 0, 0, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]); 1068 1069 // Clear cache: 1070 $dataHandler->registerRecordIdForPageCacheClearing($table, $id); 1071 // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!): 1072 if (!$swapIntoWS && $t3ver_state['curVersion'] > 0) { 1073 // For delete + completely delete! 1074 if ($table === 'pages') { 1075 // Note on fifth argument false: At this point both $curVersion and $swapVersion page records are 1076 // identical in DB. deleteEl() would now usually find all records assigned to our obsolete 1077 // page which at the same time belong to our current version page, and would delete them. 1078 // To suppress this, false tells deleteEl() to only delete the obsolete page but not its assigned records. 1079 $dataHandler->deleteEl($table, $swapWith, true, true, false); 1080 } else { 1081 $dataHandler->deleteEl($table, $swapWith, true, true); 1082 } 1083 } 1084 1085 //Update reference index for live workspace too: 1086 /** @var \TYPO3\CMS\Core\Database\ReferenceIndex $refIndexObj */ 1087 $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class); 1088 $refIndexObj->setWorkspaceId(0); 1089 $refIndexObj->updateRefIndexTable($table, $id); 1090 $refIndexObj->updateRefIndexTable($table, $swapWith); 1091 } 1092 } 1093 1094 /** 1095 * Writes remapped foreign field (IRRE). 1096 * 1097 * @param RelationHandler $dbAnalysis Instance that holds the sorting order of child records 1098 * @param array $configuration The TCA field configuration 1099 * @param int $parentId The uid of the parent record 1100 */ 1101 public function writeRemappedForeignField(RelationHandler $dbAnalysis, array $configuration, $parentId) 1102 { 1103 foreach ($dbAnalysis->itemArray as &$item) { 1104 if (isset($this->remappedIds[$item['table']][$item['id']])) { 1105 $item['id'] = $this->remappedIds[$item['table']][$item['id']]; 1106 } 1107 } 1108 $dbAnalysis->writeForeignField($configuration, $parentId); 1109 } 1110 1111 /** 1112 * Processes fields of a record for the publishing/swapping process. 1113 * Basically this takes care of IRRE (type "inline") child references. 1114 * 1115 * @param string $tableName Table name 1116 * @param string $fieldName: Field name 1117 * @param array $configuration TCA field configuration 1118 * @param array $liveData: Live record data 1119 * @param array $versionData: Version record data 1120 * @param DataHandler $dataHandler Calling data-handler object 1121 */ 1122 protected function version_swap_processFields($tableName, $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler) 1123 { 1124 $inlineType = $dataHandler->getInlineFieldType($configuration); 1125 if ($inlineType !== 'field') { 1126 return; 1127 } 1128 $foreignTable = $configuration['foreign_table']; 1129 // Read relations that point to the current record (e.g. live record): 1130 $liveRelations = $this->createRelationHandlerInstance(); 1131 $liveRelations->setWorkspaceId(0); 1132 $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration); 1133 // Read relations that point to the record to be swapped with e.g. draft record): 1134 $versionRelations = $this->createRelationHandlerInstance(); 1135 $versionRelations->setUseLiveReferenceIds(false); 1136 $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration); 1137 // Update relations for both (workspace/versioning) sites: 1138 if (count($liveRelations->itemArray)) { 1139 $dataHandler->addRemapAction( 1140 $tableName, 1141 $liveData['uid'], 1142 [$this, 'updateInlineForeignFieldSorting'], 1143 [$tableName, $liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace] 1144 ); 1145 } 1146 if (count($versionRelations->itemArray)) { 1147 $dataHandler->addRemapAction( 1148 $tableName, 1149 $liveData['uid'], 1150 [$this, 'updateInlineForeignFieldSorting'], 1151 [$tableName, $liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0] 1152 ); 1153 } 1154 } 1155 1156 /** 1157 * Updates foreign field sorting values of versioned and live 1158 * parents after(!) the whole structure has been published. 1159 * 1160 * This method is used as callback function in 1161 * DataHandlerHook::version_swap_procBasedOnFieldType(). 1162 * Sorting fields ("sortby") are not modified during the 1163 * workspace publishing/swapping process directly. 1164 * 1165 * @param string $parentTableName 1166 * @param string $parentId 1167 * @param string $foreignTableName 1168 * @param int[] $foreignIds 1169 * @param array $configuration 1170 * @param int $targetWorkspaceId 1171 * @internal 1172 */ 1173 public function updateInlineForeignFieldSorting($parentTableName, $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId) 1174 { 1175 $remappedIds = []; 1176 // Use remapped ids (live id <-> version id) 1177 foreach ($foreignIds as $foreignId) { 1178 if (!empty($this->remappedIds[$foreignTableName][$foreignId])) { 1179 $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId]; 1180 } else { 1181 $remappedIds[] = $foreignId; 1182 } 1183 } 1184 1185 $relationHandler = $this->createRelationHandlerInstance(); 1186 $relationHandler->setWorkspaceId($targetWorkspaceId); 1187 $relationHandler->setUseLiveReferenceIds(false); 1188 $relationHandler->start(implode(',', $remappedIds), $foreignTableName); 1189 $relationHandler->processDeletePlaceholder(); 1190 $relationHandler->writeForeignField($configuration, $parentId); 1191 } 1192 1193 /** 1194 * Release version from this workspace (and into "Live" workspace but as an offline version). 1195 * 1196 * @param string $table Table name 1197 * @param int $id Record UID 1198 * @param bool $flush If set, will completely delete element 1199 * @param DataHandler $dataHandler DataHandler object 1200 */ 1201 protected function version_clearWSID($table, $id, $flush = false, DataHandler $dataHandler) 1202 { 1203 if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) { 1204 $dataHandler->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1); 1205 return; 1206 } 1207 if (!$dataHandler->checkRecordUpdateAccess($table, $id)) { 1208 $dataHandler->newlog('Attempt to reset workspace for record failed because you do not have edit access', 1); 1209 return; 1210 } 1211 $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state'); 1212 if (!$liveRec) { 1213 return; 1214 } 1215 // Clear workspace ID: 1216 $updateData = [ 1217 't3ver_wsid' => 0, 1218 't3ver_tstamp' => $GLOBALS['EXEC_TIME'] 1219 ]; 1220 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); 1221 $connection->update( 1222 $table, 1223 $updateData, 1224 ['uid' => (int)$id] 1225 ); 1226 1227 // Clear workspace ID for live version AND DELETE IT as well because it is a new record! 1228 if ( 1229 VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER) 1230 || VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER) 1231 ) { 1232 $connection->update( 1233 $table, 1234 $updateData, 1235 ['uid' => (int)$liveRec['uid']] 1236 ); 1237 1238 // THIS assumes that the record was placeholder ONLY for ONE record (namely $id) 1239 $dataHandler->deleteEl($table, $liveRec['uid'], true); 1240 } 1241 // If "deleted" flag is set for the version that got released 1242 // it doesn't make sense to keep that "placeholder" anymore and we delete it completly. 1243 $wsRec = BackendUtility::getRecord($table, $id); 1244 if ( 1245 $flush 1246 || ( 1247 VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER) 1248 || VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER) 1249 ) 1250 ) { 1251 $dataHandler->deleteEl($table, $id, true, true); 1252 } 1253 // Remove the move-placeholder if found for live record. 1254 if (BackendUtility::isTableWorkspaceEnabled($table)) { 1255 if ($plhRec = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid')) { 1256 $dataHandler->deleteEl($table, $plhRec['uid'], true, true); 1257 } 1258 } 1259 } 1260 1261 /** 1262 * In case a sys_workspace_stage record is deleted we do a hard reset 1263 * for all existing records in that stage to avoid that any of these end up 1264 * as orphan records. 1265 * 1266 * @param int $stageId Elements with this stage are resetted 1267 */ 1268 protected function resetStageOfElements($stageId) 1269 { 1270 foreach ($this->getTcaTables() as $tcaTable) { 1271 if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) { 1272 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1273 ->getQueryBuilderForTable($tcaTable); 1274 1275 $queryBuilder 1276 ->update($tcaTable) 1277 ->set('t3ver_stage', StagesService::STAGE_EDIT_ID) 1278 ->where( 1279 $queryBuilder->expr()->eq( 1280 't3ver_stage', 1281 $queryBuilder->createNamedParameter($stageId, \PDO::PARAM_INT) 1282 ), 1283 $queryBuilder->expr()->eq( 1284 'pid', 1285 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) 1286 ), 1287 $queryBuilder->expr()->gt( 1288 't3ver_wsid', 1289 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 1290 ) 1291 ) 1292 ->execute(); 1293 } 1294 } 1295 } 1296 1297 /** 1298 * Flushes elements of a particular workspace to avoid orphan records. 1299 * 1300 * @param int $workspaceId The workspace to be flushed 1301 */ 1302 protected function flushWorkspaceElements($workspaceId) 1303 { 1304 $command = []; 1305 foreach ($this->getTcaTables() as $tcaTable) { 1306 if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) { 1307 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1308 ->getQueryBuilderForTable($tcaTable); 1309 $queryBuilder->getRestrictions() 1310 ->removeAll() 1311 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 1312 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, $workspaceId, false)); 1313 1314 $result = $queryBuilder 1315 ->select('uid') 1316 ->from($tcaTable) 1317 ->orderBy('uid') 1318 ->execute(); 1319 1320 while (($recordId = $result->fetchColumn()) !== false) { 1321 $command[$tcaTable][$recordId]['version']['action'] = 'flush'; 1322 } 1323 } 1324 } 1325 if (!empty($command)) { 1326 $dataHandler = $this->getDataHandler(); 1327 $dataHandler->start([], $command); 1328 $dataHandler->process_cmdmap(); 1329 } 1330 } 1331 1332 /** 1333 * Gets all defined TCA tables. 1334 * 1335 * @return array 1336 */ 1337 protected function getTcaTables() 1338 { 1339 return array_keys($GLOBALS['TCA']); 1340 } 1341 1342 /** 1343 * @return DataHandler 1344 */ 1345 protected function getDataHandler() 1346 { 1347 return GeneralUtility::makeInstance(DataHandler::class); 1348 } 1349 1350 /** 1351 * Flushes the workspace cache for current workspace and for the virtual "all workspaces" too. 1352 * 1353 * @param int $workspaceId The workspace to be flushed in cache 1354 */ 1355 protected function flushWorkspaceCacheEntriesByWorkspaceId($workspaceId) 1356 { 1357 $workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache'); 1358 $workspacesCache->flushByTag($workspaceId); 1359 $workspacesCache->flushByTag(WorkspaceService::SELECT_ALL_WORKSPACES); 1360 } 1361 1362 /******************************* 1363 ***** helper functions ****** 1364 *******************************/ 1365 1366 /** 1367 * Finds all elements for swapping versions in workspace 1368 * 1369 * @param string $table Table name of the original element to swap 1370 * @param int $id UID of the original element to swap (online) 1371 * @param int $offlineId As above but offline 1372 * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID 1373 */ 1374 public function findPageElementsForVersionSwap($table, $id, $offlineId) 1375 { 1376 $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid'); 1377 $workspaceId = (int)$rec['t3ver_wsid']; 1378 $elementData = []; 1379 if ($workspaceId === 0) { 1380 return $elementData; 1381 } 1382 // Get page UID for LIVE and workspace 1383 if ($table !== 'pages') { 1384 $rec = BackendUtility::getRecord($table, $id, 'pid'); 1385 $pageId = $rec['pid']; 1386 $rec = BackendUtility::getRecord('pages', $pageId); 1387 BackendUtility::workspaceOL('pages', $rec, $workspaceId); 1388 $offlinePageId = $rec['_ORIG_uid']; 1389 } else { 1390 $pageId = $id; 1391 $offlinePageId = $offlineId; 1392 } 1393 // Traversing all tables supporting versioning: 1394 foreach ($GLOBALS['TCA'] as $table => $cfg) { 1395 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') { 1396 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1397 ->getQueryBuilderForTable($table); 1398 1399 $queryBuilder->getRestrictions() 1400 ->removeAll() 1401 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1402 1403 $statement = $queryBuilder 1404 ->select('A.uid AS offlineUid', 'B.uid AS uid') 1405 ->from($table, 'A') 1406 ->from($table, 'B') 1407 ->where( 1408 $queryBuilder->expr()->eq( 1409 'A.pid', 1410 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) 1411 ), 1412 $queryBuilder->expr()->eq( 1413 'B.pid', 1414 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT) 1415 ), 1416 $queryBuilder->expr()->eq( 1417 'A.t3ver_wsid', 1418 $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) 1419 ), 1420 $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) 1421 ) 1422 ->execute(); 1423 1424 while ($row = $statement->fetch()) { 1425 $elementData[$table][] = [$row['uid'], $row['offlineUid']]; 1426 } 1427 } 1428 } 1429 if ($offlinePageId && $offlinePageId != $pageId) { 1430 $elementData['pages'][] = [$pageId, $offlinePageId]; 1431 } 1432 1433 return $elementData; 1434 } 1435 1436 /** 1437 * Searches for all elements from all tables on the given pages in the same workspace. 1438 * 1439 * @param array $pageIdList List of PIDs to search 1440 * @param int $workspaceId Workspace ID 1441 * @param array $elementList List of found elements. Key is table name, value is array of element UIDs 1442 */ 1443 public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList) 1444 { 1445 if ($workspaceId == 0) { 1446 return; 1447 } 1448 // Traversing all tables supporting versioning: 1449 foreach ($GLOBALS['TCA'] as $table => $cfg) { 1450 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') { 1451 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1452 ->getQueryBuilderForTable($table); 1453 1454 $queryBuilder->getRestrictions() 1455 ->removeAll() 1456 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1457 1458 $statement = $queryBuilder 1459 ->select('A.uid') 1460 ->from($table, 'A') 1461 ->from($table, 'B') 1462 ->where( 1463 $queryBuilder->expr()->eq( 1464 'A.pid', 1465 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) 1466 ), 1467 $queryBuilder->expr()->in( 1468 'B.pid', 1469 $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY) 1470 ), 1471 $queryBuilder->expr()->eq( 1472 'A.t3ver_wsid', 1473 $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) 1474 ), 1475 $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) 1476 ) 1477 ->groupBy('A.uid') 1478 ->execute(); 1479 1480 while ($row = $statement->fetch()) { 1481 $elementList[$table][] = $row['uid']; 1482 } 1483 if (is_array($elementList[$table])) { 1484 // Yes, it is possible to get non-unique array even with DISTINCT above! 1485 // It happens because several UIDs are passed in the array already. 1486 $elementList[$table] = array_unique($elementList[$table]); 1487 } 1488 } 1489 } 1490 } 1491 1492 /** 1493 * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code> 1494 * 1495 * @param string $table Table to search 1496 * @param array $idList List of records' UIDs 1497 * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publisg DRAFT from ws module! 1498 * @param array $pageIdList List of found page UIDs 1499 * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs 1500 */ 1501 public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList) 1502 { 1503 if ($workspaceId == 0) { 1504 return; 1505 } 1506 1507 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1508 ->getQueryBuilderForTable($table); 1509 $queryBuilder->getRestrictions() 1510 ->removeAll() 1511 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1512 1513 $statement = $queryBuilder 1514 ->select('B.pid') 1515 ->from($table, 'A') 1516 ->from($table, 'B') 1517 ->where( 1518 $queryBuilder->expr()->eq( 1519 'A.pid', 1520 $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) 1521 ), 1522 $queryBuilder->expr()->eq( 1523 'A.t3ver_wsid', 1524 $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) 1525 ), 1526 $queryBuilder->expr()->in( 1527 'A.uid', 1528 $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY) 1529 ), 1530 $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) 1531 ) 1532 ->groupBy('B.pid') 1533 ->execute(); 1534 1535 while ($row = $statement->fetch()) { 1536 $pageIdList[] = $row['pid']; 1537 // Find ws version 1538 // Note: cannot use BackendUtility::getRecordWSOL() 1539 // here because it does not accept workspace id! 1540 $rec = BackendUtility::getRecord('pages', $row[0]); 1541 BackendUtility::workspaceOL('pages', $rec, $workspaceId); 1542 if ($rec['_ORIG_uid']) { 1543 $elementList['pages'][$row[0]] = $rec['_ORIG_uid']; 1544 } 1545 } 1546 // The line below is necessary even with DISTINCT 1547 // because several elements can be passed by caller 1548 $pageIdList = array_unique($pageIdList); 1549 } 1550 1551 /** 1552 * Finds real page IDs for state change. 1553 * 1554 * @param array $idList List of page UIDs, possibly versioned 1555 */ 1556 public function findRealPageIds(array &$idList) 1557 { 1558 foreach ($idList as $key => $id) { 1559 $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid'); 1560 if ($rec['t3ver_oid'] > 0) { 1561 $idList[$key] = $rec['t3ver_oid']; 1562 } 1563 } 1564 } 1565 1566 /** 1567 * Creates a move placeholder for workspaces. 1568 * USE ONLY INTERNALLY 1569 * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER 1570 * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace. 1571 * 1572 * @param string $table Table name to move 1573 * @param int $uid Record uid to move (online record) 1574 * @param int $destPid Position to move to: $destPid: >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if 1575 * @param int $wsUid UID of offline version of online record 1576 * @param DataHandler $dataHandler DataHandler object 1577 * @see moveRecord() 1578 */ 1579 protected function moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $dataHandler) 1580 { 1581 // If a record gets moved after a record that already has a placeholder record 1582 // then the new placeholder record needs to be after the existing one 1583 $originalRecordDestinationPid = $destPid; 1584 if ($destPid < 0) { 1585 $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid'); 1586 if ($movePlaceHolder !== false) { 1587 $destPid = -$movePlaceHolder['uid']; 1588 } 1589 } 1590 if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) { 1591 // If already a placeholder exists, move it: 1592 $dataHandler->moveRecord_raw($table, $plh['uid'], $destPid); 1593 } else { 1594 // First, we create a placeholder record in the Live workspace that 1595 // represents the position to where the record is eventually moved to. 1596 $newVersion_placeholderFieldArray = []; 1597 1598 $factory = GeneralUtility::makeInstance( 1599 PlaceholderShadowColumnsResolver::class, 1600 $table, 1601 $GLOBALS['TCA'][$table] ?? [] 1602 ); 1603 $shadowColumns = $factory->forMovePlaceholder(); 1604 // Set values from the versioned record to the move placeholder 1605 if (!empty($shadowColumns)) { 1606 $versionedRecord = BackendUtility::getRecord($table, $wsUid); 1607 foreach ($shadowColumns as $shadowColumn) { 1608 if (isset($versionedRecord[$shadowColumn])) { 1609 $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn]; 1610 } 1611 } 1612 } 1613 1614 if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) { 1615 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME']; 1616 } 1617 if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) { 1618 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $dataHandler->userid; 1619 } 1620 if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) { 1621 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME']; 1622 } 1623 if ($table === 'pages') { 1624 // Copy page access settings from original page to placeholder 1625 $perms_clause = $dataHandler->BE_USER->getPagePermsClause(Permission::PAGE_SHOW); 1626 $access = BackendUtility::readPageAccess($uid, $perms_clause); 1627 $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid']; 1628 $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid']; 1629 $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user']; 1630 $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group']; 1631 $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody']; 1632 } 1633 $newVersion_placeholderFieldArray['t3ver_label'] = 'MovePlaceholder #' . $uid; 1634 $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid; 1635 // Setting placeholder state value for temporary record 1636 $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER); 1637 // Setting workspace - only so display of place holders can filter out those from other workspaces. 1638 $newVersion_placeholderFieldArray['t3ver_wsid'] = $dataHandler->BE_USER->workspace; 1639 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $dataHandler->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid); 1640 // moving localized records requires to keep localization-settings for the placeholder too 1641 if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) { 1642 $l10nParentRec = BackendUtility::getRecord($table, $uid); 1643 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; 1644 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]; 1645 if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) { 1646 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]; 1647 } 1648 unset($l10nParentRec); 1649 } 1650 // Initially, create at root level. 1651 $newVersion_placeholderFieldArray['pid'] = 0; 1652 $id = 'NEW_MOVE_PLH'; 1653 // Saving placeholder as 'original' 1654 $dataHandler->insertDB($table, $id, $newVersion_placeholderFieldArray, false); 1655 // Move the new placeholder from temporary root-level to location: 1656 $dataHandler->moveRecord_raw($table, $dataHandler->substNEWwithIDs[$id], $destPid); 1657 // Move the workspace-version of the original to be the version of the move-to-placeholder: 1658 // Setting placeholder state value for version (so it can know it is currently a new version...) 1659 $updateFields = [ 1660 't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER) 1661 ]; 1662 1663 GeneralUtility::makeInstance(ConnectionPool::class) 1664 ->getConnectionForTable($table) 1665 ->update( 1666 $table, 1667 $updateFields, 1668 ['uid' => (int)$wsUid] 1669 ); 1670 } 1671 // Check for the localizations of that element and move them as well 1672 $dataHandler->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid); 1673 } 1674 1675 /** 1676 * Gets an instance of the command map helper. 1677 * 1678 * @param DataHandler $dataHandler DataHandler object 1679 * @return CommandMap 1680 */ 1681 public function getCommandMap(DataHandler $dataHandler) 1682 { 1683 return GeneralUtility::makeInstance( 1684 CommandMap::class, 1685 $this, 1686 $dataHandler, 1687 $dataHandler->cmdmap, 1688 $dataHandler->BE_USER->workspace 1689 ); 1690 } 1691 1692 /** 1693 * Returns all fieldnames from a table which have the unique evaluation type set. 1694 * 1695 * @param string $table Table name 1696 * @return array Array of fieldnames 1697 */ 1698 protected function getUniqueFields($table) 1699 { 1700 $listArr = []; 1701 if (empty($GLOBALS['TCA'][$table]['columns'])) { 1702 return $listArr; 1703 } 1704 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) { 1705 if ($configArr['config']['type'] === 'input') { 1706 $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true); 1707 if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) { 1708 $listArr[] = $field; 1709 } 1710 } 1711 } 1712 return $listArr; 1713 } 1714 1715 /** 1716 * @return RelationHandler 1717 */ 1718 protected function createRelationHandlerInstance() 1719 { 1720 return GeneralUtility::makeInstance(RelationHandler::class); 1721 } 1722 1723 /** 1724 * @return LanguageService 1725 */ 1726 protected function getLanguageService() 1727 { 1728 return $GLOBALS['LANG']; 1729 } 1730} 1731