/** * Swapping versions of a record * 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 * * @param string $table Table name * @param int $id UID of the online record to swap * @param int $swapWith UID of the archived version to swap with! * @param bool $swapIntoWS If set, swaps online into workspace instead of publishing out of workspace. * @param DataHandler $dataHandler DataHandler object * @param string $comment Notification comment * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email? * @param array $notificationAlternativeRecipients comma separated list of recipients to notificate instead of normal be_users * @return void */ protected function version_swap($table, $id, $swapWith, $swapIntoWS = 0, DataHandler $dataHandler, $comment = '', $notificationEmailInfo = false, $notificationAlternativeRecipients = []) { // Check prerequisites before start swapping // Skip records that have been deleted during the current execution if ($dataHandler->hasDeletedRecord($table, $id)) { return; } // First, check if we may actually edit the online record if (!$dataHandler->checkRecordUpdateAccess($table, $id)) { $dataHandler->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1); return; } // Select the two versions: $curVersion = BackendUtility::getRecord($table, $id, '*'); $swapVersion = BackendUtility::getRecord($table, $swapWith, '*'); $movePlh = []; $movePlhID = 0; if (!(is_array($curVersion) && is_array($swapVersion))) { $dataHandler->newlog('Error: Either online or swap version could not be selected!', 2); return; } if (!$dataHandler->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) { $dataHandler->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1); return; } $wsAccess = $dataHandler->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']); if (!($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int) $swapVersion['t3ver_stage'] === -10)) { $dataHandler->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1); return; } if (!($dataHandler->doesRecordExist($table, $swapWith, 'show') && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) { $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', 1); return; } if ($swapIntoWS && !$dataHandler->BE_USER->workspaceSwapAccess()) { $dataHandler->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1); return; } // Check if the swapWith record really IS a version of the original! if (!((int) $swapVersion['pid'] == -1 && (int) $curVersion['pid'] >= 0 && (int) $swapVersion['t3ver_oid'] === (int) $id)) { $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); return; } // Lock file name: $lockFileName = PATH_site . 'typo3temp/var/swap_locking/' . $table . '_' . $id . '.ser'; if (@is_file($lockFileName)) { $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); return; } // Now start to swap records by first creating the lock file // Write lock-file: GeneralUtility::writeFileToTypo3tempDir($lockFileName, serialize(['tstamp' => $GLOBALS['EXEC_TIME'], 'user' => $dataHandler->BE_USER->user['username'], 'curVersion' => $curVersion, 'swapVersion' => $swapVersion])); // Find fields to keep $keepFields = $this->getUniqueFields($table); if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby']; } // l10n-fields must be kept otherwise the localization // will be lost during the publishing if ($table !== 'pages_language_overlay' && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) { $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; } // Swap "keepfields" foreach ($keepFields as $fN) { $tmp = $swapVersion[$fN]; $swapVersion[$fN] = $curVersion[$fN]; $curVersion[$fN] = $tmp; } // Preserve states: $t3ver_state = []; $t3ver_state['swapVersion'] = $swapVersion['t3ver_state']; $t3ver_state['curVersion'] = $curVersion['t3ver_state']; // Modify offline version to become online: $tmp_wsid = $swapVersion['t3ver_wsid']; // Set pid for ONLINE $swapVersion['pid'] = (int) $curVersion['pid']; // We clear this because t3ver_oid only make sense for offline versions // and we want to prevent unintentional misuse of this // value for online records. $swapVersion['t3ver_oid'] = 0; // In case of swapping and the offline record has a state // (like 2 or 4 for deleting or move-pointer) we set the // current workspace ID so the record is not deselected // in the interface by BackendUtility::versioningPlaceholderClause() $swapVersion['t3ver_wsid'] = 0; if ($swapIntoWS) { if ($t3ver_state['swapVersion'] > 0) { $swapVersion['t3ver_wsid'] = $dataHandler->BE_USER->workspace; } else { $swapVersion['t3ver_wsid'] = (int) $curVersion['t3ver_wsid']; } } $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME']; $swapVersion['t3ver_stage'] = 0; if (!$swapIntoWS) { $swapVersion['t3ver_state'] = (string) new VersionState(VersionState::DEFAULT_STATE); } // Moving element. if (BackendUtility::isTableWorkspaceEnabled($table)) { // && $t3ver_state['swapVersion']==4 // Maybe we don't need this? if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) { $movePlhID = $plhRec['uid']; $movePlh['pid'] = $swapVersion['pid']; $swapVersion['pid'] = (int) $plhRec['pid']; $curVersion['t3ver_state'] = (int) $swapVersion['t3ver_state']; $swapVersion['t3ver_state'] = (string) new VersionState(VersionState::DEFAULT_STATE); if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { // sortby is a "keepFields" which is why this will work... $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']]; $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']]; } } } // Take care of relations in each field (e.g. IRRE): if (is_array($GLOBALS['TCA'][$table]['columns'])) { foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) { $this->version_swap_processFields($table, $field, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler); } } unset($swapVersion['uid']); // Modify online version to become offline: unset($curVersion['uid']); // Set pid for OFFLINE $curVersion['pid'] = -1; $curVersion['t3ver_oid'] = (int) $id; $curVersion['t3ver_wsid'] = $swapIntoWS ? (int) $tmp_wsid : 0; $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME']; $curVersion['t3ver_count'] = $curVersion['t3ver_count'] + 1; // Increment lifecycle counter $curVersion['t3ver_stage'] = 0; if (!$swapIntoWS) { $curVersion['t3ver_state'] = (string) new VersionState(VersionState::DEFAULT_STATE); } // Registering and swapping MM relations in current and swap records: $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith); // Generating proper history data to prepare logging $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion); $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion); // Execute swapping: $sqlErrors = []; $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); $connection->update($table, $swapVersion, ['uid' => (int) $id]); if ($connection->errorCode()) { $sqlErrors[] = $connection->errorInfo(); } else { $connection->update($table, $curVersion, ['uid' => (int) $swapWith]); if ($connection->errorCode()) { $sqlErrors[] = $connection->errorInfo(); } else { unlink($lockFileName); } } if (!empty($sqlErrors)) { $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2); } else { // Register swapped ids for later remapping: $this->remappedIds[$table][$id] = $swapWith; $this->remappedIds[$table][$swapWith] = $id; // If a moving operation took place...: if ($movePlhID) { // Remove, if normal publishing: if (!$swapIntoWS) { // For delete + completely delete! $dataHandler->deleteEl($table, $movePlhID, true, true); } else { // Otherwise update the movePlaceholder: GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table)->update($table, $movePlh, ['uid' => (int) $movePlhID]); $dataHandler->addRemapStackRefIndex($table, $movePlhID); } } // Checking for delete: // Delete only if new/deleted placeholders are there. if (!$swapIntoWS && ((int) $t3ver_state['swapVersion'] === 1 || (int) $t3ver_state['swapVersion'] === 2)) { // Force delete $dataHandler->deleteEl($table, $id, true); } $dataHandler->newlog2(($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']); // Update reference index of the live record: $dataHandler->addRemapStackRefIndex($table, $id); // Set log entry for live record: $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion); if ($propArr['_ORIG_pid'] == -1) { $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated'); } else { $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated'); } $theLogId = $dataHandler->log($table, $id, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']); $dataHandler->setHistory($table, $id, $theLogId); // Update reference index of the offline record: $dataHandler->addRemapStackRefIndex($table, $swapWith); // Set log entry for offline record: $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion); if ($propArr['_ORIG_pid'] == -1) { $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated'); } else { $label = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated'); } $theLogId = $dataHandler->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']); $dataHandler->setHistory($table, $swapWith, $theLogId); $stageId = -20; // \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_EXECUTE_ID; if ($notificationEmailInfo) { $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment; $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment]; $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = $table . ':' . $id; $this->notificationEmailInfo[$notificationEmailInfoKey]['alternativeRecipients'] = $notificationAlternativeRecipients; } else { $this->notifyStageChange($wsAccess, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients); } // Write to log with stageId -20 $dataHandler->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id); $dataHandler->log($table, $id, 6, 0, 0, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]); // Clear cache: $dataHandler->registerRecordIdForPageCacheClearing($table, $id); // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!): if (!$swapIntoWS && $t3ver_state['curVersion'] > 0) { // For delete + completely delete! $dataHandler->deleteEl($table, $swapWith, true, true); } //Update reference index for live workspace too: /** @var $refIndexObj \TYPO3\CMS\Core\Database\ReferenceIndex */ $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class); $refIndexObj->setWorkspaceId(0); $refIndexObj->updateRefIndexTable($table, $id); $refIndexObj->updateRefIndexTable($table, $swapWith); } }