diff --git a/composer.json b/composer.json index d987be06496390e7060baa04f93d380dfd851f14..62a4f1b859905a7ff0e6d0599ed9661feb0fe74f 100644 --- a/composer.json +++ b/composer.json @@ -77,7 +77,6 @@ "typo3/sysext/fluid/Migrations/Code/ClassAliasMap.php", "typo3/sysext/info/Migrations/Code/ClassAliasMap.php", "typo3/sysext/lowlevel/Migrations/Code/ClassAliasMap.php", - "typo3/sysext/version/Migrations/Code/ClassAliasMap.php", "typo3/sysext/workspaces/Migrations/Code/ClassAliasMap.php" ] }, @@ -170,7 +169,6 @@ "TYPO3\\CMS\\T3editor\\": "typo3/sysext/t3editor/Classes/", "TYPO3\\CMS\\Taskcenter\\": "typo3/sysext/taskcenter/Classes/", "TYPO3\\CMS\\Tstemplate\\": "typo3/sysext/tstemplate/Classes/", - "TYPO3\\CMS\\Version\\": "typo3/sysext/version/Classes/", "TYPO3\\CMS\\Viewpage\\": "typo3/sysext/viewpage/Classes/", "TYPO3\\CMS\\Workspaces\\": "typo3/sysext/workspaces/Classes/" }, diff --git a/composer.lock b/composer.lock index d90f8d8ccca29e029d6d20a12e0e62b2d350c094..772b4029db10349dc633054e2b7ce1d03199fbf4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "0e757db5e2ea6409800d3e505af4f744", + "content-hash": "b603bc5f3ec745ae1db133cca1fc6d46", "packages": [ { "name": "cogpowered/finediff", diff --git a/typo3/sysext/backend/Classes/Utility/BackendUtility.php b/typo3/sysext/backend/Classes/Utility/BackendUtility.php index 02faf4a9050f48fd447f8dd163cddf01fdb41c51..1c11d1070f1565b3387f6e586db1a766f2886a33 100644 --- a/typo3/sysext/backend/Classes/Utility/BackendUtility.php +++ b/typo3/sysext/backend/Classes/Utility/BackendUtility.php @@ -3841,7 +3841,7 @@ class BackendUtility */ public static function fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch = false) { - if (!ExtensionManagementUtility::isLoaded('version')) { + if (!ExtensionManagementUtility::isLoaded('workspaces')) { return; } // Check that the input record is an offline version from a table that supports versioning: @@ -3903,7 +3903,7 @@ class BackendUtility */ public static function workspaceOL($table, &$row, $wsid = -99, $unsetMovePointers = false) { - if (!ExtensionManagementUtility::isLoaded('version')) { + if (!ExtensionManagementUtility::isLoaded('workspaces')) { return; } // If this is FALSE the placeholder is shown raw in the backend. @@ -4021,7 +4021,7 @@ class BackendUtility */ public static function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*') { - if (ExtensionManagementUtility::isLoaded('version')) { + if (ExtensionManagementUtility::isLoaded('workspaces')) { if ($workspace !== 0 && $GLOBALS['TCA'][$table] && self::isTableWorkspaceEnabled($table)) { // Select workspace version of record: @@ -4084,7 +4084,7 @@ class BackendUtility */ public static function getLiveVersionIdOfRecord($table, $uid) { - if (!ExtensionManagementUtility::isLoaded('version')) { + if (!ExtensionManagementUtility::isLoaded('workspaces')) { return null; } $liveVersionId = null; diff --git a/typo3/sysext/backend/Classes/View/PageLayoutView.php b/typo3/sysext/backend/Classes/View/PageLayoutView.php index 8a548010f449253fdb48420e8c7e6735bf24759a..7df137cb510970cdfe92e5108c205c438d6b1d1d 100644 --- a/typo3/sysext/backend/Classes/View/PageLayoutView.php +++ b/typo3/sysext/backend/Classes/View/PageLayoutView.php @@ -3875,9 +3875,8 @@ class PageLayoutView implements LoggerAwareInterface if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['sortby']; } - if (ExtensionManagementUtility::isLoaded( - 'version' - ) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { + if (ExtensionManagementUtility::isLoaded('workspaces') + && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { $fieldListArr[] = 't3ver_id'; $fieldListArr[] = 't3ver_state'; $fieldListArr[] = 't3ver_wsid'; diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php index 3a383d097a0d80d9d25202d55799b0d88d7b2a17..f3a3e1f86dd4eab29d1adf45c5ccb5654f91b5a8 100644 --- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php +++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php @@ -9113,11 +9113,11 @@ class DataHandler implements LoggerAwareInterface */ protected function createRelationHandlerInstance() { - $isVersionLoaded = ExtensionManagementUtility::isLoaded('version'); + $isWorkspacesLoaded = ExtensionManagementUtility::isLoaded('workspaces'); $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); $relationHandler->setWorkspaceId($this->BE_USER->workspace); - $relationHandler->setUseLiveReferenceIds($isVersionLoaded); - $relationHandler->setUseLiveParentIds($isVersionLoaded); + $relationHandler->setUseLiveReferenceIds($isWorkspacesLoaded); + $relationHandler->setUseLiveParentIds($isWorkspacesLoaded); return $relationHandler; } diff --git a/typo3/sysext/core/Classes/DataHandling/PlainDataResolver.php b/typo3/sysext/core/Classes/DataHandling/PlainDataResolver.php index ba335d32335ba3946f9096431cfcffcde95e9e4a..cbe90cf8784bc708ea65a416367d5edb7d27ea9b 100644 --- a/typo3/sysext/core/Classes/DataHandling/PlainDataResolver.php +++ b/typo3/sysext/core/Classes/DataHandling/PlainDataResolver.php @@ -372,7 +372,7 @@ class PlainDataResolver */ protected function isWorkspaceEnabled() { - if (ExtensionManagementUtility::isLoaded('version')) { + if (ExtensionManagementUtility::isLoaded('workspaces')) { return BackendUtility::isTableWorkspaceEnabled($this->tableName); } return false; diff --git a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php index debe42abe12994b226b8fb3bb7df5e7608c60902..b4989b44b4ed169ae8a9985f7d0ebfe46855fe60 100644 --- a/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php +++ b/typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php @@ -112,6 +112,11 @@ class ExtensionManagementUtility */ public static function isLoaded($key, $exitOnError = false) { + // safety net for extensions checking for "EXT:version", can be removed in TYPO3 v10. + if ($key === 'version') { + trigger_error('EXT:version has been moved into EXT:workspaces, you should check against "workspaces", as this might lead to unexpected behaviour in the future.', E_USER_DEPRECATED); + $key = 'workspaces'; + } $isLoaded = static::$packageManager->isPackageActive($key); if ($exitOnError && !$isLoaded) { throw new \BadFunctionCallException('TYPO3 Fatal Error: Extension "' . $key . '" is not loaded!', 1270853910); diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-82896-SystemExtensionVersionMigratedIntoWorkspaces.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-82896-SystemExtensionVersionMigratedIntoWorkspaces.rst new file mode 100644 index 0000000000000000000000000000000000000000..e864148c660b464bd4cfe2d4ccc07178f503deb2 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Breaking-82896-SystemExtensionVersionMigratedIntoWorkspaces.rst @@ -0,0 +1,54 @@ +.. include:: ../../Includes.txt + +======================================================================== +Breaking: #82896 - System extension "version" migrated into "workspaces" +======================================================================== + +See :issue:`82896` + +Description +=========== + +The basic functionality of versioning records, previously located within the "version" system +extension was moved into the "workspaces" extension, which not only enhances the versioning with +workflows and workflow stages, but also adds a Backend module to configure and to publish versioned +records within a workspace. + +The extensions' deeply coupled logic is now moved into one system extension, providing the same +functionality still. + + +Impact +====== + +Using the versioning functionality of TYPO3 is now coupled with the workspace and workflow logic, +and cannot be used separately for custom versioning strategies not supported by TYPO3 Core. + +Additionally, third-party extensions checking for the previously available "version" extensions +will trigger a deprecation warning. + + +Affected Installations +====================== + +Any installation solely providing versioning functionality based on the "version" extension, +but not using "workspaces". + + +Migration +========= + +Adapt your changes to check for "workspaces" instead of the "version" extension. + +.. code-block:: php + + # old + if (ExtensionManagementUtility::isLoaded('version')) { ... } + + # new + if (ExtensionManagementUtility::isLoaded('workspaces')) { ... } + +If you built custom functionality built on "version" without "workspaces", ensure to adapt +your settings and old class names to use the workspace PHP namespaces. + +.. index:: PHP-API, NotScanned \ No newline at end of file diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/ext_emconf.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/ext_emconf.php index a3b5e1c64826757f9ff3f04230ea9c5cb5a64d44..d0daa7cbb102ac721388f793e97d262bc5190a5e 100644 --- a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/ext_emconf.php +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/ext_emconf.php @@ -15,7 +15,6 @@ $EM_CONF[$_EXTKEY] = [ 'depends' => [ 'typo3' => '4.5.0-0.0.0', 'workspaces' => '0.0.0-', - 'version' => '0.0.0-', ], 'conflicts' => [ ], diff --git a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_datahandler/ext_emconf.php b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_datahandler/ext_emconf.php index e5ce8ffff6ba1c8fbe54c68433d4fbb346b283df..abfcffc004c62e4a2ccc5216db022261316c7161 100644 --- a/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_datahandler/ext_emconf.php +++ b/typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_datahandler/ext_emconf.php @@ -15,7 +15,6 @@ $EM_CONF[$_EXTKEY] = [ 'depends' => [ 'typo3' => '6.0.0-0.0.0', 'workspaces' => '0.0.0-', - 'version' => '0.0.0-', ], 'conflicts' => [ ], diff --git a/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php index 381b521a842307fedf78ad49a91a7ca45c2714ce..e3c79fec27d03419efadc2cd5f4475b96605d861 100644 --- a/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php +++ b/typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php @@ -1252,7 +1252,7 @@ class AbstractDatabaseRecordList extends AbstractRecordList if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) { $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['sortby']; } - if (ExtensionManagementUtility::isLoaded('version') && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { + if (ExtensionManagementUtility::isLoaded('workspaces') && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { $fieldListArr[] = 't3ver_id'; $fieldListArr[] = 't3ver_state'; $fieldListArr[] = 't3ver_wsid'; diff --git a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php index 686403a5b8f6c755cd3696d0866f2cdbfe675c1d..3d5f0c7ccce8970c51b1f613157fdb74f936d9a3 100644 --- a/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php +++ b/typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php @@ -1842,7 +1842,7 @@ class DatabaseRecordList { $module = $this->getModule(); $rowUid = $row['uid']; - if (ExtensionManagementUtility::isLoaded('version') && isset($row['_ORIG_uid'])) { + if (ExtensionManagementUtility::isLoaded('workspaces') && isset($row['_ORIG_uid'])) { $rowUid = $row['_ORIG_uid']; } $cells = [ @@ -3650,7 +3650,7 @@ class DatabaseRecordList $fieldListArr[] = $GLOBALS['TCA'][$table]['ctrl']['sortby']; } if (ExtensionManagementUtility::isLoaded( - 'version' + 'workspaces' ) && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { $fieldListArr[] = 't3ver_id'; $fieldListArr[] = 't3ver_state'; diff --git a/typo3/sysext/version/Classes/Hook/DataHandlerHook.php b/typo3/sysext/version/Classes/Hook/DataHandlerHook.php deleted file mode 100644 index 65547869f36f2ab4f89efbc07d215c13a64d6bba..0000000000000000000000000000000000000000 --- a/typo3/sysext/version/Classes/Hook/DataHandlerHook.php +++ /dev/null @@ -1,1592 +0,0 @@ -<?php -namespace TYPO3\CMS\Version\Hook; - -/* - * This file is part of the TYPO3 CMS project. - * - * It is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, either version 2 - * of the License, or any later version. - * - * For the full copyright and license information, please read the - * LICENSE.txt file that was distributed with this source code. - * - * The TYPO3 project - inspiring people to share! - */ - -use Doctrine\DBAL\DBALException; -use Doctrine\DBAL\Platforms\SQLServerPlatform; -use TYPO3\CMS\Backend\Utility\BackendUtility; -use TYPO3\CMS\Core\Database\Connection; -use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; -use TYPO3\CMS\Core\Database\ReferenceIndex; -use TYPO3\CMS\Core\DataHandling\DataHandler; -use TYPO3\CMS\Core\Localization\LanguageService; -use TYPO3\CMS\Core\Service\MarkerBasedTemplateService; -use TYPO3\CMS\Core\Utility\ArrayUtility; -use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Core\Versioning\VersionState; - -/** - * Contains some parts for staging, versioning and workspaces - * to interact with the TYPO3 Core Engine - */ -class DataHandlerHook -{ - /** - * For accumulating information about workspace stages raised - * on elements so a single mail is sent as notification. - * previously called "accumulateForNotifEmail" in DataHandler - * - * @var array - */ - protected $notificationEmailInfo = []; - - /** - * Contains remapped IDs. - * - * @var array - */ - protected $remappedIds = []; - - /** - * @var \TYPO3\CMS\Workspaces\Service\WorkspaceService - */ - protected $workspaceService; - - /**************************** - ***** Cmdmap Hooks ****** - ****************************/ - /** - * hook that is called before any cmd of the commandmap is executed - * - * @param DataHandler $dataHandler reference to the main DataHandler object - */ - public function processCmdmap_beforeStart(DataHandler $dataHandler) - { - // Reset notification array - $this->notificationEmailInfo = []; - // Resolve dependencies of version/workspaces actions: - $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get(); - } - - /** - * hook that is called when no prepared command was found - * - * @param string $command the command to be executed - * @param string $table the table of the record - * @param int $id the ID of the record - * @param mixed $value the value containing the data - * @param bool $commandIsProcessed can be set so that other hooks or - * @param DataHandler $dataHandler reference to the main DataHandler object - */ - public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler) - { - // custom command "version" - if ($command === 'version') { - $commandIsProcessed = true; - $action = (string)$value['action']; - $comment = !empty($value['comment']) ? $value['comment'] : ''; - $notificationAlternativeRecipients = (isset($value['notificationAlternativeRecipients'])) && is_array($value['notificationAlternativeRecipients']) ? $value['notificationAlternativeRecipients'] : []; - switch ($action) { - case 'new': - $dataHandler->versionizeRecord($table, $id, $value['label']); - break; - case 'swap': - $this->version_swap( - $table, - $id, - $value['swapWith'], - $value['swapIntoWS'], - $dataHandler, - $comment, - true, - $notificationAlternativeRecipients - ); - break; - case 'clearWSID': - $this->version_clearWSID($table, $id, false, $dataHandler); - break; - case 'flush': - $this->version_clearWSID($table, $id, true, $dataHandler); - break; - case 'setStage': - $elementIds = GeneralUtility::trimExplode(',', $id, true); - foreach ($elementIds as $elementId) { - $this->version_setStage( - $table, - $elementId, - $value['stageId'], - $comment, - true, - $dataHandler, - $notificationAlternativeRecipients - ); - } - break; - default: - // Do nothing - } - } - } - - /** - * hook that is called AFTER all commands of the commandmap was - * executed - * - * @param DataHandler $dataHandler reference to the main DataHandler object - */ - public function processCmdmap_afterFinish(DataHandler $dataHandler) - { - // Empty accumulation array: - foreach ($this->notificationEmailInfo as $notifItem) { - $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $dataHandler, $notifItem['alternativeRecipients']); - } - // Reset notification array - $this->notificationEmailInfo = []; - // Reset remapped IDs - $this->remappedIds = []; - } - - /** - * hook that is called when an element shall get deleted - * - * @param string $table the table of the record - * @param int $id the ID of the record - * @param array $record The accordant database record - * @param bool $recordWasDeleted can be set so that other hooks or - * @param DataHandler $dataHandler reference to the main DataHandler object - */ - public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler) - { - // only process the hook if it wasn't processed - // by someone else before - if ($recordWasDeleted) { - return; - } - $recordWasDeleted = true; - // For Live version, try if there is a workspace version because if so, rather "delete" that instead - // Look, if record is an offline version, then delete directly: - if ($record['pid'] != -1) { - if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) { - $record = $wsVersion; - $id = $record['uid']; - } - } - $recordVersionState = VersionState::cast($record['t3ver_state']); - // Look, if record is an offline version, then delete directly: - if ($record['pid'] == -1) { - if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { - // In Live workspace, delete any. In other workspaces there must be match. - if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) { - $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state'); - // Processing can be skipped if a delete placeholder shall be swapped/published - // during the current request. Thus it will be deleted later on... - $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']); - if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid']) - && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action']) - && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith']) - && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap' - && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id - ) { - return null; - } - - if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) { - // Change normal versioned record to delete placeholder - // Happens when an edited record is deleted - GeneralUtility::makeInstance(ConnectionPool::class) - ->getConnectionForTable($table) - ->update( - $table, - [ - 't3ver_label' => 'DELETED!', - 't3ver_state' => 2, - ], - ['uid' => $id] - ); - - // Delete localization overlays: - $dataHandler->deleteL10nOverlayRecords($table, $id); - } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) { - // Delete those in WS 0 + if their live records state was not "Placeholder". - $dataHandler->deleteEl($table, $id); - // Delete move-placeholder if current version record is a move-to-pointer - if ($recordVersionState->equals(VersionState::MOVE_POINTER)) { - $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']); - if (!empty($movePlaceholder)) { - $dataHandler->deleteEl($table, $movePlaceholder['uid']); - } - } - } else { - // If live record was placeholder (new/deleted), rather clear - // it from workspace (because it clears both version and placeholder). - $this->version_clearWSID($table, $id, false, $dataHandler); - } - } else { - $dataHandler->newlog('Tried to delete record from another workspace', 1); - } - } else { - $dataHandler->newlog('Versioning not enabled for record with PID = -1!', 2); - } - } elseif ($res = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($record['pid'], $table)) { - // Look, if record is "online" or in a versionized branch, then delete directly. - if ($res > 0) { - $dataHandler->deleteEl($table, $id); - } else { - $dataHandler->newlog('Stage of root point did not allow for deletion', 1); - } - } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) { - // Placeholders for moving operations are deletable directly. - // Get record which its a placeholder for and reset the t3ver_state of that: - if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) { - // Clear the state flag of the workspace version of the record - // Setting placeholder state value for version (so it can know it is currently a new version...) - - GeneralUtility::makeInstance(ConnectionPool::class) - ->getConnectionForTable($table) - ->update( - $table, - [ - 't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE) - ], - ['uid' => (int)$wsRec['uid']] - ); - } - $dataHandler->deleteEl($table, $id); - } else { - // Otherwise, try to delete by versioning: - $copyMappingArray = $dataHandler->copyMappingArray; - $dataHandler->versionizeRecord($table, $id, 'DELETED!', true); - // Determine newly created versions: - // (remove placeholders are copied and modified, thus they appear in the copyMappingArray) - $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray); - // Delete localization overlays: - foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) { - foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) { - $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId); - } - } - } - } - - /** - * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about - * moving records that are *not* in the live workspace - * - * @param string $table the table of the record - * @param int $uid the ID of the record - * @param int $destPid Position to move to: $destPid: >=0 then it points to - * @param array $propArr Record properties, like header and pid (includes workspace overlay) - * @param array $moveRec Record properties, like header and pid (without workspace overlay) - * @param int $resolvedPid The final page ID of the record - * @param bool $recordWasMoved can be set so that other hooks or - * @param DataHandler $dataHandler - */ - public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler) - { - // Only do something in Draft workspace - if ($dataHandler->BE_USER->workspace === 0) { - return; - } - if ($destPid < 0) { - // Fetch move placeholder, since it might point to a new page in the current workspace - $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid'); - if ($movePlaceHolder !== false) { - $resolvedPid = $movePlaceHolder['pid']; - } - } - $recordWasMoved = true; - $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']); - // Get workspace version of the source record, if any: - $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid'); - // Handle move-placeholders if the current record is not one already - if ( - BackendUtility::isTableWorkspaceEnabled($table) - && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER) - ) { - // Create version of record first, if it does not exist - if (empty($WSversion['uid'])) { - $dataHandler->versionizeRecord($table, $uid, 'MovePointer'); - $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid'); - $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid); - } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) { - // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders - $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid); - } - } - // Check workspace permissions: - $workspaceAccessBlocked = []; - // Element was in "New/Deleted/Moved" so it can be moved... - $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder(); - $destRes = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table); - $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableWorkspaceEnabled($table)); - // Workspace source check: - if (!$recIsNewVersion) { - $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid); - if ($errorCode) { - $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' '; - } elseif (!$canMoveRecord && $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) { - $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" '; - } - } - // Workspace destination check: - // All records can be inserted if $destRes is greater than zero. - // Only new versions can be inserted if $destRes is FALSE. - // NO RECORDS can be inserted if $destRes is negative which indicates a stage - // not allowed for use. If "versioningWS" is version 2, moving can take place of versions. - // since TYPO3 CMS 7, version2 is the default and the only option - if (!($destRes > 0 || $canMoveRecord && !$destRes)) { - $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" '; - } elseif ($destRes == 1 && $WSversion['uid']) { - $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID '; - } - if (empty($workspaceAccessBlocked)) { - // If the move operation is done on a versioned record, which is - // NOT new/deleted placeholder and versioningWS is in version 2, then... - // since TYPO3 CMS 7, version2 is the default and the only option - if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) { - $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $dataHandler); - } else { - // moving not needed, just behave like in live workspace - $recordWasMoved = false; - } - } else { - $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1); - } - } - - /** - * Processes fields of a moved record and follows references. - * - * @param DataHandler $dataHandler Calling DataHandler instance - * @param int $resolvedPageId Resolved real destination page id - * @param string $table Name of parent table - * @param int $uid UID of the parent record - */ - protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid) - { - $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid); - if (empty($versionedRecord)) { - return; - } - foreach ($versionedRecord as $field => $value) { - if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) { - continue; - } - $this->moveRecord_processFieldValue( - $dataHandler, - $resolvedPageId, - $table, - $uid, - $field, - $value, - $GLOBALS['TCA'][$table]['columns'][$field]['config'] - ); - } - } - - /** - * Processes a single field of a moved record and follows references. - * - * @param DataHandler $dataHandler Calling DataHandler instance - * @param int $resolvedPageId Resolved real destination page id - * @param string $table Name of parent table - * @param int $uid UID of the parent record - * @param string $field Name of the field of the parent record - * @param string $value Value of the field of the parent record - * @param array $configuration TCA field configuration of the parent record - */ - protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration) - { - $inlineFieldType = $dataHandler->getInlineFieldType($configuration); - $inlineProcessing = ( - ($inlineFieldType === 'list' || $inlineFieldType === 'field') - && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table']) - && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent']) - ); - - if ($inlineProcessing) { - if ($table === 'pages') { - // If the inline elements are related to a page record, - // make sure they reside at that page and not at its parent - $resolvedPageId = $uid; - } - - $dbAnalysis = $this->createRelationHandlerInstance(); - $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration); - - // Moving records to a positive destination will insert each - // record at the beginning, thus the order is reversed here: - foreach ($dbAnalysis->itemArray as $item) { - $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state'); - if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) { - continue; - } - $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId); - } - } - } - - /**************************** - ***** Notifications ****** - ****************************/ - /** - * Send an email notification to users in workspace - * - * @param array $stat Workspace access array from \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::checkWorkspace() - * @param int $stageId New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected! - * @param string $table Table name of element (or list of element names if $id is zero) - * @param int $id Record uid of element (if zero, then $table is used as reference to element(s) alone) - * @param string $comment User comment sent along with action - * @param DataHandler $dataHandler DataHandler object - * @param array $notificationAlternativeRecipients List of recipients to notify instead of be_users selected by sys_workspace, list is generated by workspace extension module - */ - protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = []) - { - $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']); - // So, if $id is not set, then $table is taken to be the complete element name! - $elementName = $id ? $table . ':' . $id : $table; - if (!is_array($workspaceRec)) { - return; - } - - // Get the new stage title from workspaces library, if workspaces extension is installed - if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) { - $stageService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class); - $newStage = $stageService->getStageTitle((int)$stageId); - } else { - // @todo CONSTANTS SHOULD BE USED - tx_service_workspace_workspaces - // @todo use localized labels - // Compile label: - switch ((int)$stageId) { - case 1: - $newStage = 'Ready for review'; - break; - case 10: - $newStage = 'Ready for publishing'; - break; - case -1: - $newStage = 'Element was rejected!'; - break; - case 0: - $newStage = 'Rejected element was noticed and edited'; - break; - default: - $newStage = 'Unknown state change!?'; - } - } - if (empty($notificationAlternativeRecipients)) { - // Compile list of recipients: - $emails = []; - switch ((int)$stat['stagechg_notification']) { - case 1: - switch ((int)$stageId) { - case 1: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']); - break; - case 10: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); - break; - case -1: - // List of elements to reject: - $allElements = explode(',', $elementName); - // Traverse them, and find the history of each - foreach ($allElements as $elRef) { - list($eTable, $eUid) = explode(':', $elRef); - - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable('sys_log'); - - $queryBuilder->getRestrictions()->removeAll(); - - $result = $queryBuilder - ->select('log_data', 'tstamp', 'userid') - ->from('sys_log') - ->where( - $queryBuilder->expr()->eq( - 'action', - $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'details_nr', - $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'tablename', - $queryBuilder->createNamedParameter($eTable, \PDO::PARAM_STR) - ), - $queryBuilder->expr()->eq( - 'recuid', - $queryBuilder->createNamedParameter($eUid, \PDO::PARAM_INT) - ) - ) - ->orderBy('uid', 'DESC') - ->execute(); - - // Find all implicated since the last stage-raise from editing to review: - while ($dat = $result->fetch()) { - $data = unserialize($dat['log_data']); - $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails; - if ($data['stage'] == 1) { - break; - } - } - } - break; - case 0: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']); - break; - default: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); - } - break; - case 10: - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails; - $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails; - break; - default: - // Do nothing - } - } else { - $emails = $notificationAlternativeRecipients; - } - // prepare and then send the emails - if (!empty($emails)) { - // Path to record is found: - list($elementTable, $elementUid) = explode(':', $elementName); - $elementUid = (int)$elementUid; - $elementRecord = BackendUtility::getRecord($elementTable, $elementUid); - $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord); - if ($elementTable === 'pages') { - $pageUid = $elementUid; - } else { - BackendUtility::fixVersioningPid($elementTable, $elementRecord); - $pageUid = ($elementUid = $elementRecord['pid']); - } - - // new way, options are - // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject - // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject - $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid); - $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.']; - $markers = [ - '###RECORD_TITLE###' => $recordTitle, - '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20), - '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], - '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir, - '###WORKSPACE_TITLE###' => $workspaceRec['title'], - '###WORKSPACE_UID###' => $workspaceRec['uid'], - '###ELEMENT_NAME###' => $elementName, - '###NEXT_STAGE###' => $newStage, - '###COMMENT###' => $comment, - // See: #30212 - keep both markers for compatibility - '###USER_REALNAME###' => $dataHandler->BE_USER->user['realName'], - '###USER_FULLNAME###' => $dataHandler->BE_USER->user['realName'], - '###USER_USERNAME###' => $dataHandler->BE_USER->user['username'] - ]; - // add marker for preview links if workspace extension is loaded - if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) { - $this->workspaceService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class); - // only generate the link if the marker is in the template - prevents database from getting to much entries - if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) { - $tempEmailMessage = $this->getLanguageService()->sL($emailConfig['message']); - } else { - $tempEmailMessage = $emailConfig['message']; - } - if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) { - $markers['###PREVIEW_LINK###'] = $this->workspaceService->generateWorkspacePreviewLink($elementUid); - } - unset($tempEmailMessage); - $markers['###SPLITTED_PREVIEW_LINK###'] = $this->workspaceService->generateWorkspaceSplittedPreviewLink($elementUid, true); - } - // Hook for preprocessing of the content for formmails: - if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'])) { - foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] as $className) { - $_procObj = GeneralUtility::makeInstance($className); - $markers = $_procObj->postModifyMarkers($markers, $this); - } - } - // send an email to each individual user, to ensure the - // multilanguage version of the email - $emailRecipients = []; - // an array of language objects that are needed - // for emails with different languages - $languageObjects = [ - $this->getLanguageService()->lang => $this->getLanguageService() - ]; - // loop through each recipient and send the email - foreach ($emails as $recipientData) { - // don't send an email twice - if (isset($emailRecipients[$recipientData['email']])) { - continue; - } - $emailSubject = $emailConfig['subject']; - $emailMessage = $emailConfig['message']; - $emailRecipients[$recipientData['email']] = $recipientData['email']; - // check if the email needs to be localized - // in the users' language - if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) { - $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default'; - if (!isset($languageObjects[$recipientLanguage])) { - // a LANG object in this language hasn't been - // instantiated yet, so this is done here - /** @var $languageObject \TYPO3\CMS\Core\Localization\LanguageService */ - $languageObject = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Localization\LanguageService::class); - $languageObject->init($recipientLanguage); - $languageObjects[$recipientLanguage] = $languageObject; - } else { - $languageObject = $languageObjects[$recipientLanguage]; - } - if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) { - $emailSubject = $languageObject->sL($emailSubject); - } - if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) { - $emailMessage = $languageObject->sL($emailMessage); - } - } - $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class); - $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true); - $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true); - // Send an email to the recipient - /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */ - $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class); - if (!empty($recipientData['realName'])) { - $recipient = [$recipientData['email'] => $recipientData['realName']]; - } else { - $recipient = $recipientData['email']; - } - $mail->setTo($recipient) - ->setSubject($emailSubject) - ->setBody($emailMessage); - $mail->send(); - } - $emailRecipients = implode(',', $emailRecipients); - $dataHandler->newlog2('Notification email for stage change was sent to "' . $emailRecipients . '"', $table, $id); - } - } - - /** - * Return be_users that should be notified on stage change from input list. - * previously called notifyStageChange_getEmails() in DataHandler - * - * @param string $listOfUsers List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set. - * @param bool $noTablePrefix If set, the input list are integers and not strings. - * @return array Array of emails - */ - protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = false) - { - $users = GeneralUtility::trimExplode(',', $listOfUsers, true); - $emails = []; - foreach ($users as $userIdent) { - if ($noTablePrefix) { - $id = (int)$userIdent; - } else { - list($table, $id) = GeneralUtility::revExplode('_', $userIdent, 2); - } - if ($table === 'be_users' || $noTablePrefix) { - if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) { - if (trim($userRecord['email']) !== '') { - $emails[$id] = $userRecord; - } - } - } - } - return $emails; - } - - /**************************** - ***** Stage Changes ****** - ****************************/ - /** - * Setting stage of record - * - * @param string $table Table name - * @param int $integer Record UID - * @param int $stageId Stage ID to set - * @param string $comment Comment that goes into log - * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email? - * @param DataHandler $dataHandler DataHandler object - * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users - */ - protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = false, DataHandler $dataHandler, array $notificationAlternativeRecipients = []) - { - if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) { - $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, 1); - } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) { - $record = BackendUtility::getRecord($table, $id); - $stat = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']); - // check if the usere is allowed to the current stage, so it's also allowed to send to next stage - if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) { - // Set stage of record: - GeneralUtility::makeInstance(ConnectionPool::class) - ->getConnectionForTable($table) - ->update( - $table, - [ - 't3ver_stage' => $stageId, - ], - ['uid' => (int)$id] - ); - $dataHandler->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id); - // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere! - $dataHandler->log($table, $id, 6, 0, 0, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]); - if ((int)$stat['stagechg_notification'] > 0) { - if ($notificationEmailInfo) { - $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment]; - $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id; - $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients; - } else { - $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients); - } - } - } else { - $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1); - } - } else { - $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', 1); - } - } - - /***************************** - ***** CMD versioning ****** - *****************************/ - - /** - * 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 - */ - 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); - - $platform = $connection->getDatabasePlatform(); - $tableDetails = null; - if ($platform instanceof SQLServerPlatform) { - // mssql needs to set proper PARAM_LOB and others to update fields - $tableDetails = $connection->getSchemaManager()->listTableDetails($table); - } - - try { - $types = []; - - if ($platform instanceof SQLServerPlatform) { - foreach ($curVersion as $columnName => $columnValue) { - $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType(); - } - } - - $connection->update( - $table, - $swapVersion, - ['uid' => (int)$id], - $types - ); - } catch (DBALException $e) { - $sqlErrors[] = $e->getPrevious()->getMessage(); - } - - if (empty($sqlErrors)) { - try { - $types = []; - if ($platform instanceof SQLServerPlatform) { - foreach ($curVersion as $columnName => $columnValue) { - $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType(); - } - } - - $connection->update( - $table, - $curVersion, - ['uid' => (int)$swapWith], - $types - ); - unlink($lockFileName); - } catch (DBALException $e) { - $sqlErrors[] = $e->getPrevious()->getMessage(); - } - } - - 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); - } - } - - /** - * Writes remapped foreign field (IRRE). - * - * @param \TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis Instance that holds the sorting order of child records - * @param array $configuration The TCA field configuration - * @param int $parentId The uid of the parent record - */ - public function writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId) - { - foreach ($dbAnalysis->itemArray as &$item) { - if (isset($this->remappedIds[$item['table']][$item['id']])) { - $item['id'] = $this->remappedIds[$item['table']][$item['id']]; - } - } - $dbAnalysis->writeForeignField($configuration, $parentId); - } - - /** - * Processes fields of a record for the publishing/swapping process. - * Basically this takes care of IRRE (type "inline") child references. - * - * @param string $tableName Table name - * @param string $fieldName: Field name - * @param array $configuration TCA field configuration - * @param array $liveData: Live record data - * @param array $versionData: Version record data - * @param DataHandler $dataHandler Calling data-handler object - */ - protected function version_swap_processFields($tableName, $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler) - { - $inlineType = $dataHandler->getInlineFieldType($configuration); - if ($inlineType !== 'field') { - return; - } - $foreignTable = $configuration['foreign_table']; - // Read relations that point to the current record (e.g. live record): - $liveRelations = $this->createRelationHandlerInstance(); - $liveRelations->setWorkspaceId(0); - $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration); - // Read relations that point to the record to be swapped with e.g. draft record): - $versionRelations = $this->createRelationHandlerInstance(); - $versionRelations->setUseLiveReferenceIds(false); - $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration); - // Update relations for both (workspace/versioning) sites: - if (count($liveRelations->itemArray)) { - $dataHandler->addRemapAction( - $tableName, - $liveData['uid'], - [$this, 'updateInlineForeignFieldSorting'], - [$tableName, $liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace] - ); - } - if (count($versionRelations->itemArray)) { - $dataHandler->addRemapAction( - $tableName, - $liveData['uid'], - [$this, 'updateInlineForeignFieldSorting'], - [$tableName, $liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0] - ); - } - } - - /** - * Updates foreign field sorting values of versioned and live - * parents after(!) the whole structure has been published. - * - * This method is used as callback function in - * DataHandlerHook::version_swap_procBasedOnFieldType(). - * Sorting fields ("sortby") are not modified during the - * workspace publishing/swapping process directly. - * - * @param string $parentTableName - * @param string $parentId - * @param string $foreignTableName - * @param int[] $foreignIds - * @param array $configuration - * @param int $targetWorkspaceId - * @internal - */ - public function updateInlineForeignFieldSorting($parentTableName, $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId) - { - $remappedIds = []; - // Use remapped ids (live id <-> version id) - foreach ($foreignIds as $foreignId) { - if (!empty($this->remappedIds[$foreignTableName][$foreignId])) { - $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId]; - } else { - $remappedIds[] = $foreignId; - } - } - - $relationHandler = $this->createRelationHandlerInstance(); - $relationHandler->setWorkspaceId($targetWorkspaceId); - $relationHandler->setUseLiveReferenceIds(false); - $relationHandler->start(implode(',', $remappedIds), $foreignTableName); - $relationHandler->processDeletePlaceholder(); - $relationHandler->writeForeignField($configuration, $parentId); - } - - /** - * Release version from this workspace (and into "Live" workspace but as an offline version). - * - * @param string $table Table name - * @param int $id Record UID - * @param bool $flush If set, will completely delete element - * @param DataHandler $dataHandler DataHandler object - */ - protected function version_clearWSID($table, $id, $flush = false, DataHandler $dataHandler) - { - if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) { - $dataHandler->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1); - return; - } - if (!$dataHandler->checkRecordUpdateAccess($table, $id)) { - $dataHandler->newlog('Attempt to reset workspace for record failed because you do not have edit access', 1); - return; - } - $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state'); - if (!$liveRec) { - return; - } - // Clear workspace ID: - $updateData = [ - 't3ver_wsid' => 0, - 't3ver_tstamp' => $GLOBALS['EXEC_TIME'] - ]; - $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); - $connection->update( - $table, - $updateData, - ['uid' => (int)$id] - ); - - // Clear workspace ID for live version AND DELETE IT as well because it is a new record! - if ( - VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER) - || VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER) - ) { - $connection->update( - $table, - $updateData, - ['uid' => (int)$liveRec['uid']] - ); - - // THIS assumes that the record was placeholder ONLY for ONE record (namely $id) - $dataHandler->deleteEl($table, $liveRec['uid'], true); - } - // If "deleted" flag is set for the version that got released - // it doesn't make sense to keep that "placeholder" anymore and we delete it completly. - $wsRec = BackendUtility::getRecord($table, $id); - if ( - $flush - || ( - VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER) - || VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER) - ) - ) { - $dataHandler->deleteEl($table, $id, true, true); - } - // Remove the move-placeholder if found for live record. - if (BackendUtility::isTableWorkspaceEnabled($table)) { - if ($plhRec = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid')) { - $dataHandler->deleteEl($table, $plhRec['uid'], true, true); - } - } - } - - /******************************* - ***** helper functions ****** - *******************************/ - - /** - * Finds all elements for swapping versions in workspace - * - * @param string $table Table name of the original element to swap - * @param int $id UID of the original element to swap (online) - * @param int $offlineId As above but offline - * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID - */ - public function findPageElementsForVersionSwap($table, $id, $offlineId) - { - $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid'); - $workspaceId = (int)$rec['t3ver_wsid']; - $elementData = []; - if ($workspaceId === 0) { - return $elementData; - } - // Get page UID for LIVE and workspace - if ($table !== 'pages') { - $rec = BackendUtility::getRecord($table, $id, 'pid'); - $pageId = $rec['pid']; - $rec = BackendUtility::getRecord('pages', $pageId); - BackendUtility::workspaceOL('pages', $rec, $workspaceId); - $offlinePageId = $rec['_ORIG_uid']; - } else { - $pageId = $id; - $offlinePageId = $offlineId; - } - // Traversing all tables supporting versioning: - foreach ($GLOBALS['TCA'] as $table => $cfg) { - if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable($table); - - $queryBuilder->getRestrictions() - ->removeAll() - ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - - $statement = $queryBuilder - ->select('A.uid AS offlineUid', 'B.uid AS uid') - ->from($table, 'A') - ->from($table, 'B') - ->where( - $queryBuilder->expr()->eq( - 'A.pid', - $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'B.pid', - $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'A.t3ver_wsid', - $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) - ) - ->execute(); - - while ($row = $statement->fetch()) { - $elementData[$table][] = [$row['uid'], $row['offlineUid']]; - } - } - } - if ($offlinePageId && $offlinePageId != $pageId) { - $elementData['pages'][] = [$pageId, $offlinePageId]; - } - - return $elementData; - } - - /** - * Searches for all elements from all tables on the given pages in the same workspace. - * - * @param array $pageIdList List of PIDs to search - * @param int $workspaceId Workspace ID - * @param array $elementList List of found elements. Key is table name, value is array of element UIDs - */ - public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList) - { - if ($workspaceId == 0) { - return; - } - // Traversing all tables supporting versioning: - foreach ($GLOBALS['TCA'] as $table => $cfg) { - if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable($table); - - $queryBuilder->getRestrictions() - ->removeAll() - ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - - $statement = $queryBuilder - ->select('A.uid') - ->from($table, 'A') - ->from($table, 'B') - ->where( - $queryBuilder->expr()->eq( - 'A.pid', - $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->in( - 'B.pid', - $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY) - ), - $queryBuilder->expr()->eq( - 'A.t3ver_wsid', - $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) - ) - ->groupBy('A.uid') - ->execute(); - - while ($row = $statement->fetch()) { - $elementList[$table][] = $row['uid']; - } - if (is_array($elementList[$table])) { - // Yes, it is possible to get non-unique array even with DISTINCT above! - // It happens because several UIDs are passed in the array already. - $elementList[$table] = array_unique($elementList[$table]); - } - } - } - } - - /** - * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code> - * - * @param string $table Table to search - * @param array $idList List of records' UIDs - * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publisg DRAFT from ws module! - * @param array $pageIdList List of found page UIDs - * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs - */ - public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList) - { - if ($workspaceId == 0) { - return; - } - - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) - ->getQueryBuilderForTable($table); - $queryBuilder->getRestrictions() - ->removeAll() - ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); - - $statement = $queryBuilder - ->select('B.pid') - ->from($table, 'A') - ->from($table, 'B') - ->where( - $queryBuilder->expr()->eq( - 'A.pid', - $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->eq( - 'A.t3ver_wsid', - $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) - ), - $queryBuilder->expr()->in( - 'A.uid', - $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY) - ), - $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) - ) - ->groupBy('B.pid') - ->execute(); - - while ($row = $statement->fetch()) { - $pageIdList[] = $row['pid']; - // Find ws version - // Note: cannot use BackendUtility::getRecordWSOL() - // here because it does not accept workspace id! - $rec = BackendUtility::getRecord('pages', $row[0]); - BackendUtility::workspaceOL('pages', $rec, $workspaceId); - if ($rec['_ORIG_uid']) { - $elementList['pages'][$row[0]] = $rec['_ORIG_uid']; - } - } - // The line below is necessary even with DISTINCT - // because several elements can be passed by caller - $pageIdList = array_unique($pageIdList); - } - - /** - * Finds real page IDs for state change. - * - * @param array $idList List of page UIDs, possibly versioned - */ - public function findRealPageIds(array &$idList) - { - foreach ($idList as $key => $id) { - $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid'); - if ($rec['t3ver_oid'] > 0) { - $idList[$key] = $rec['t3ver_oid']; - } - } - } - - /** - * Creates a move placeholder for workspaces. - * USE ONLY INTERNALLY - * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER - * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace. - * - * @param string $table Table name to move - * @param int $uid Record uid to move (online record) - * @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 - * @param int $wsUid UID of offline version of online record - * @param DataHandler $dataHandler DataHandler object - * @see moveRecord() - */ - protected function moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $dataHandler) - { - // If a record gets moved after a record that already has a placeholder record - // then the new placeholder record needs to be after the existing one - $originalRecordDestinationPid = $destPid; - if ($destPid < 0) { - $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid'); - if ($movePlaceHolder !== false) { - $destPid = -$movePlaceHolder['uid']; - } - } - if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) { - // If already a placeholder exists, move it: - $dataHandler->moveRecord_raw($table, $plh['uid'], $destPid); - } else { - // First, we create a placeholder record in the Live workspace that - // represents the position to where the record is eventually moved to. - $newVersion_placeholderFieldArray = []; - - // Use property for move placeholders if set (since TYPO3 CMS 6.2) - if (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'])) { - $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders']; - } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'])) { - // Fallback to property for new placeholder (existed long time before TYPO3 CMS 6.2) - $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders']; - } - - // Set values from the versioned record to the move placeholder - if (!empty($shadowColumnsForMovePlaceholder)) { - $versionedRecord = BackendUtility::getRecord($table, $wsUid); - $shadowColumns = GeneralUtility::trimExplode(',', $shadowColumnsForMovePlaceholder, true); - foreach ($shadowColumns as $shadowColumn) { - if (isset($versionedRecord[$shadowColumn])) { - $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn]; - } - } - } - - if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) { - $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME']; - } - if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) { - $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $dataHandler->userid; - } - if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) { - $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME']; - } - if ($table === 'pages') { - // Copy page access settings from original page to placeholder - $perms_clause = $dataHandler->BE_USER->getPagePermsClause(1); - $access = BackendUtility::readPageAccess($uid, $perms_clause); - $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid']; - $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid']; - $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user']; - $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group']; - $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody']; - } - $newVersion_placeholderFieldArray['t3ver_label'] = 'MovePlaceholder #' . $uid; - $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid; - // Setting placeholder state value for temporary record - $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER); - // Setting workspace - only so display of place holders can filter out those from other workspaces. - $newVersion_placeholderFieldArray['t3ver_wsid'] = $dataHandler->BE_USER->workspace; - $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $dataHandler->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid); - // moving localized records requires to keep localization-settings for the placeholder too - if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) { - $l10nParentRec = BackendUtility::getRecord($table, $uid); - $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; - $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]; - if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) { - $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]; - } - unset($l10nParentRec); - } - // Initially, create at root level. - $newVersion_placeholderFieldArray['pid'] = 0; - $id = 'NEW_MOVE_PLH'; - // Saving placeholder as 'original' - $dataHandler->insertDB($table, $id, $newVersion_placeholderFieldArray, false); - // Move the new placeholder from temporary root-level to location: - $dataHandler->moveRecord_raw($table, $dataHandler->substNEWwithIDs[$id], $destPid); - // Move the workspace-version of the original to be the version of the move-to-placeholder: - // Setting placeholder state value for version (so it can know it is currently a new version...) - $updateFields = [ - 't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER) - ]; - - GeneralUtility::makeInstance(ConnectionPool::class) - ->getConnectionForTable($table) - ->update( - $table, - $updateFields, - ['uid' => (int)$wsUid] - ); - } - // Check for the localizations of that element and move them as well - $dataHandler->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid); - } - - /** - * Gets an instance of the command map helper. - * - * @param DataHandler $dataHandler DataHandler object - * @return \TYPO3\CMS\Version\DataHandler\CommandMap - */ - public function getCommandMap(DataHandler $dataHandler) - { - return GeneralUtility::makeInstance( - \TYPO3\CMS\Version\DataHandler\CommandMap::class, - $this, - $dataHandler, - $dataHandler->cmdmap, - $dataHandler->BE_USER->workspace - ); - } - - /** - * Returns all fieldnames from a table which have the unique evaluation type set. - * - * @param string $table Table name - * @return array Array of fieldnames - */ - protected function getUniqueFields($table) - { - $listArr = []; - if (empty($GLOBALS['TCA'][$table]['columns'])) { - return $listArr; - } - foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) { - if ($configArr['config']['type'] === 'input') { - $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true); - if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) { - $listArr[] = $field; - } - } - } - return $listArr; - } - - /** - * @return \TYPO3\CMS\Core\Database\RelationHandler - */ - protected function createRelationHandlerInstance() - { - return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class); - } - - /** - * @return LanguageService - */ - protected function getLanguageService() - { - return $GLOBALS['LANG']; - } -} diff --git a/typo3/sysext/version/LICENSE.txt b/typo3/sysext/version/LICENSE.txt deleted file mode 100644 index 95d36a78ffce9a9ad8c4000f87603a76dd17e67e..0000000000000000000000000000000000000000 --- a/typo3/sysext/version/LICENSE.txt +++ /dev/null @@ -1,345 +0,0 @@ -Some icons used in the TYPO3 project are retrieved from the "Silk" icon set of -Mark James, which can be found at http://famfamfam.com/lab/icons/silk/. This -set is distributed under a Creative Commons Attribution 2.5 License. The -license can be found at http://creativecommons.org/licenses/by/2.5/. ---------------------------------- - - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - <one line to give the program's name and a brief idea of what it does.> - Copyright (C) <year> <name of author> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - <signature of Ty Coon>, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/typo3/sysext/version/Migrations/Code/ClassAliasMap.php b/typo3/sysext/version/Migrations/Code/ClassAliasMap.php deleted file mode 100644 index c8ad25d385de5914ed8e5368e6a954d5f13952e4..0000000000000000000000000000000000000000 --- a/typo3/sysext/version/Migrations/Code/ClassAliasMap.php +++ /dev/null @@ -1,6 +0,0 @@ -<?php -return [ - 'TYPO3\\CMS\\Version\\Hook\\PreviewHook' => \TYPO3\CMS\Workspaces\Hook\PreviewHook::class, - 'TYPO3\\CMS\\Version\\Task\\AutoPublishTask' => \TYPO3\CMS\Workspaces\Task\AutoPublishTask::class, - 'TYPO3\\CMS\\Version\\Utility\\WorkspacesUtility' => \TYPO3\CMS\Workspaces\Service\WorkspaceService::class -]; diff --git a/typo3/sysext/version/Resources/Public/Icons/Extension.png b/typo3/sysext/version/Resources/Public/Icons/Extension.png deleted file mode 100644 index 3a7dfc08452a75bf5b2401b783c40a63e51a4324..0000000000000000000000000000000000000000 Binary files a/typo3/sysext/version/Resources/Public/Icons/Extension.png and /dev/null differ diff --git a/typo3/sysext/version/Resources/Public/Icons/module-version.svg b/typo3/sysext/version/Resources/Public/Icons/module-version.svg deleted file mode 100644 index a33d3429cfa817435aba8157f4501068f53284fd..0000000000000000000000000000000000000000 --- a/typo3/sysext/version/Resources/Public/Icons/module-version.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#7A950F" d="M0 0h64v64H0z"/><g fill="#FFF"><path fill-rule="evenodd" clip-rule="evenodd" d="M47.2 48H36.8c-1.6 0-2.9-1.3-2.9-2.9v-5.3l3.2 3.2c.7.7 1.6 1.1 2.6 1.1s1.9-.4 2.6-1.1l5.4-5.4c.7-.7 1.1-1.6 1.1-2.6v-.4c.7.5 1.2 1.4 1.2 2.3v8.3c0 1.5-1.3 2.8-2.8 2.8zm0-18h-1.8c-.4-5.3-2.4-8.7-6.1-10.2-1.3-.5-2.8-.8-4.6-.8-.2 0-.5 0-.7.1v-.2c0-1.6 1.3-2.9 2.9-2.9h10.3c1.6 0 2.9 1.3 2.9 2.9v8.3c-.1 1.5-1.4 2.8-2.9 2.8zm-18.6-9.3l-.1.1c-.3.1-.7.3-1.1.6-.9.6-1.5 1.7-1.5 2.8 0 1.8 1.5 3.3 3.4 3.3.2 0 .4 0 .7-.1-.1 1.4-1.4 2.6-2.8 2.6H16.8c-1.6 0-2.9-1.3-2.9-2.9v-8.3c0-1.6 1.3-2.9 2.9-2.9h10.3c1.6 0 2.9 1.3 2.9 2.9V20c-.4.3-.9.5-1.4.7zM16.8 34h10.3c1.6 0 2.9 1.3 2.9 2.9v8.3c0 1.6-1.3 2.9-2.9 2.9H16.8c-1.6 0-2.9-1.3-2.9-2.9v-8.3c.1-1.6 1.4-2.9 2.9-2.9z"/><path d="M34.7 22c1.2 0 2.4.1 3.5.6 3.7 1.5 4.3 5.8 4.3 9.3v2.4h2.7c.4 0 .7.3.7.7 0 .2-.1.4-.2.5l-5.4 5.4c-.2 0-.3.1-.5.1s-.4-.1-.5-.2l-5.4-5.4c-.1-.1-.2-.3-.2-.5 0-.4.3-.7.7-.7h2.7v-2.4c0-4.6-.9-7.6-5.9-7.6-.4 0-.9 0-1.3.1-.2 0-.4.1-.5.1-.2 0-.4-.1-.4-.3 0-.1.1-.2.2-.3.2-.2.6-.3.8-.4 1.2-.7 3.2-1.4 4.7-1.4z"/></g></svg> \ No newline at end of file diff --git a/typo3/sysext/version/composer.json b/typo3/sysext/version/composer.json deleted file mode 100644 index fd7dda315048de32df49ceb69f758cda4c561916..0000000000000000000000000000000000000000 --- a/typo3/sysext/version/composer.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "typo3/cms-version", - "type": "typo3-cms-framework", - "description": "Backend Interface for management of the versioning API.", - "homepage": "https://typo3.org", - "license": ["GPL-2.0+"], - "authors": [{ - "name": "TYPO3 Core Team", - "email": "typo3cms@typo3.org", - "role": "Developer" - }], - - "require": { - "typo3/cms-core": ">=9.0.0 <=9.0.99" - }, - "conflict": { - "typo3/cms": "*" - }, - "replace": { - "version": "*" - }, - "extra": { - "branch-alias": { - "dev-master": "9.x-dev" - }, - "typo3/class-alias-loader": { - "class-alias-maps": [ - "Migrations/Code/ClassAliasMap.php" - ] - }, - "typo3/cms": { - "extension-key": "version" - } - }, - "autoload": { - "psr-4": { - "TYPO3\\CMS\\Version\\": "Classes/" - } - } -} diff --git a/typo3/sysext/version/ext_emconf.php b/typo3/sysext/version/ext_emconf.php deleted file mode 100644 index 51f975230393909e8eb10ab0a03c78484fbb9162..0000000000000000000000000000000000000000 --- a/typo3/sysext/version/ext_emconf.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php -$EM_CONF[$_EXTKEY] = [ - 'title' => 'Versioning Management', - 'description' => 'Backend Interface for management of the versioning API.', - 'category' => 'be', - 'author' => 'TYPO3 Core Team', - 'author_email' => 'typo3cms@typo3.org', - 'author_company' => '', - 'state' => 'stable', - 'uploadfolder' => 0, - 'createDirs' => '', - 'clearCacheOnLoad' => 0, - 'version' => '9.0.0', - 'constraints' => [ - 'depends' => [ - 'typo3' => '9.0.0-9.0.99', - ], - 'conflicts' => [], - 'suggests' => [], - ], -]; diff --git a/typo3/sysext/version/ext_localconf.php b/typo3/sysext/version/ext_localconf.php deleted file mode 100644 index 758122964e943b0fee27c6902551e69f023aa534..0000000000000000000000000000000000000000 --- a/typo3/sysext/version/ext_localconf.php +++ /dev/null @@ -1,13 +0,0 @@ -<?php -defined('TYPO3_MODE') or die(); - -// register the hook to actually do the work within DataHandler -$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['version'] = \TYPO3\CMS\Version\Hook\DataHandlerHook::class; -$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass']['version'] = \TYPO3\CMS\Version\Hook\DataHandlerHook::class; - -// add default notification options to every page -\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(' -tx_version.workspaces.stageNotificationEmail.subject = LLL:EXT:version/Resources/Private/Language/locallang_emails.xlf:subject -tx_version.workspaces.stageNotificationEmail.message = LLL:EXT:version/Resources/Private/Language/locallang_emails.xlf:message -# tx_version.workspaces.stageNotificationEmail.additionalHeaders = -'); diff --git a/typo3/sysext/version/Classes/DataHandler/CommandMap.php b/typo3/sysext/workspaces/Classes/DataHandler/CommandMap.php similarity index 93% rename from typo3/sysext/version/Classes/DataHandler/CommandMap.php rename to typo3/sysext/workspaces/Classes/DataHandler/CommandMap.php index 3f2315f912e6ff3e8d8562f310e31747e5eb93cf..7028053fbe7e6d7808d59cf936a78e03ecb00c81 100644 --- a/typo3/sysext/version/Classes/DataHandler/CommandMap.php +++ b/typo3/sysext/workspaces/Classes/DataHandler/CommandMap.php @@ -1,5 +1,5 @@ <?php -namespace TYPO3\CMS\Version\DataHandler; +namespace TYPO3\CMS\Workspaces\DataHandler; /* * This file is part of the TYPO3 CMS project. @@ -16,7 +16,7 @@ namespace TYPO3\CMS\Version\DataHandler; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Version\Dependency\ElementEntity; +use TYPO3\CMS\Workspaces\Dependency\ElementEntity; /** * Handles the \TYPO3\CMS\Core\DataHandling\DataHandler command map and is @@ -36,7 +36,7 @@ class CommandMap const KEY_TransformDependentElementsToUseLiveId = 'KEY_TransformDependentElementsToUseLiveId'; /** - * @var \TYPO3\CMS\Version\Hook\DataHandlerHook + * @var \TYPO3\CMS\Workspaces\Hook\DataHandlerHook */ protected $parent; @@ -71,19 +71,19 @@ class CommandMap protected $scopes; /** - * @var \TYPO3\CMS\Version\Dependency\ElementEntityProcessor + * @var \TYPO3\CMS\Workspaces\Dependency\ElementEntityProcessor */ protected $elementEntityProcessor; /** * Creates this object. * - * @param \TYPO3\CMS\Version\Hook\DataHandlerHook $parent + * @param \TYPO3\CMS\Workspaces\Hook\DataHandlerHook $parent * @param \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain * @param array $commandMap * @param int $workspace */ - public function __construct(\TYPO3\CMS\Version\Hook\DataHandlerHook $parent, \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain, array $commandMap, $workspace) + public function __construct(\TYPO3\CMS\Workspaces\Hook\DataHandlerHook $parent, \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain, array $commandMap, $workspace) { $this->setParent($parent); $this->setTceMain($tceMain); @@ -108,7 +108,7 @@ class CommandMap * Sets the command map. * * @param array $commandMap - * @return \TYPO3\CMS\Version\DataHandler\CommandMap + * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap */ public function set(array $commandMap) { @@ -119,7 +119,7 @@ class CommandMap /** * Gets the parent object. * - * @return \TYPO3\CMS\Version\Hook\DataHandlerHook + * @return \TYPO3\CMS\Workspaces\Hook\DataHandlerHook */ public function getParent() { @@ -129,10 +129,10 @@ class CommandMap /** * Sets the parent object. * - * @param \TYPO3\CMS\Version\Hook\DataHandlerHook $parent - * @return \TYPO3\CMS\Version\DataHandler\CommandMap + * @param \TYPO3\CMS\Workspaces\Hook\DataHandlerHook $parent + * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap */ - public function setParent(\TYPO3\CMS\Version\Hook\DataHandlerHook $parent) + public function setParent(\TYPO3\CMS\Workspaces\Hook\DataHandlerHook $parent) { $this->parent = $parent; return $this; @@ -152,7 +152,7 @@ class CommandMap * Sets the parent object. * * @param \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain - * @return \TYPO3\CMS\Version\DataHandler\CommandMap + * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap */ public function setTceMain(\TYPO3\CMS\Core\DataHandling\DataHandler $tceMain) { @@ -185,7 +185,7 @@ class CommandMap * (see options.workspaces.swapMode). * * @param string $workspacesSwapMode - * @return \TYPO3\CMS\Version\DataHandler\CommandMap + * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap */ public function setWorkspacesSwapMode($workspacesSwapMode) { @@ -198,7 +198,7 @@ class CommandMap * see options.workspaces.changeStageMode) * * @param string $workspacesChangeStageMode - * @return \TYPO3\CMS\Version\DataHandler\CommandMap + * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap */ public function setWorkspacesChangeStageMode($workspacesChangeStageMode) { @@ -209,13 +209,13 @@ class CommandMap /** * Gets the element entity processor. * - * @return \TYPO3\CMS\Version\Dependency\ElementEntityProcessor + * @return \TYPO3\CMS\Workspaces\Dependency\ElementEntityProcessor */ protected function getElementEntityProcessor() { if (!isset($this->elementEntityProcessor)) { $this->elementEntityProcessor = GeneralUtility::makeInstance( - \TYPO3\CMS\Version\Dependency\ElementEntityProcessor::class + \TYPO3\CMS\Workspaces\Dependency\ElementEntityProcessor::class ); $this->elementEntityProcessor->setWorkspace($this->getWorkspace()); } @@ -225,7 +225,7 @@ class CommandMap /** * Processes the command map. * - * @return \TYPO3\CMS\Version\DataHandler\CommandMap + * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap */ public function process() { @@ -303,12 +303,12 @@ class CommandMap /** * Adds workspaces elements for swapping/publishing. * - * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency + * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency * @param string $table * @param int $liveId * @param array $properties */ - protected function addWorkspacesSwapElements(\TYPO3\CMS\Version\Dependency\DependencyResolver $dependency, $table, $liveId, array $properties) + protected function addWorkspacesSwapElements(\TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency, $table, $liveId, array $properties) { $elementList = []; // Fetch accordant elements if the swapMode is 'any' or 'pages': @@ -408,12 +408,12 @@ class CommandMap /** * Adds workspaces elements for staging. * - * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency + * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency * @param string $table * @param string $versionId * @param array $properties */ - protected function addWorkspacesSetStageElements(\TYPO3\CMS\Version\Dependency\DependencyResolver $dependency, $table, $versionId, array $properties) + protected function addWorkspacesSetStageElements(\TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency, $table, $versionId, array $properties) { $dependency->addElement($table, $versionId, ['versionId' => $versionId, 'properties' => $properties]); } @@ -468,10 +468,10 @@ class CommandMap * Applies the workspaces dependencies and removes incomplete structures or automatically * completes them * - * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency + * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency * @param string $scope */ - protected function applyWorkspacesDependencies(\TYPO3\CMS\Version\Dependency\DependencyResolver $dependency, $scope) + protected function applyWorkspacesDependencies(\TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency, $scope) { $transformDependentElementsToUseLiveId = $this->getScopeData($scope, self::KEY_TransformDependentElementsToUseLiveId); $elementsToBeVersioned = $dependency->getElements(); @@ -680,12 +680,12 @@ class CommandMap * Gets an instance of the depency resolver utility. * * @param string $scope Scope identifier - * @return \TYPO3\CMS\Version\Dependency\DependencyResolver + * @return \TYPO3\CMS\Workspaces\Dependency\DependencyResolver */ protected function getDependencyUtility($scope) { - /** @var $dependency \TYPO3\CMS\Version\Dependency\DependencyResolver */ - $dependency = GeneralUtility::makeInstance(\TYPO3\CMS\Version\Dependency\DependencyResolver::class); + /** @var $dependency \TYPO3\CMS\Workspaces\Dependency\DependencyResolver */ + $dependency = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Dependency\DependencyResolver::class); $dependency->setWorkspace($this->getWorkspace()); $dependency->setOuterMostParentsRequireReferences(true); if ($this->getScopeData($scope, self::KEY_ElementConstructCallback)) { @@ -782,12 +782,12 @@ class CommandMap * * @param string $method * @param array $targetArguments - * @return \TYPO3\CMS\Version\Dependency\EventCallback + * @return \TYPO3\CMS\Workspaces\Dependency\EventCallback */ protected function getDependencyCallback($method, array $targetArguments = []) { return GeneralUtility::makeInstance( - \TYPO3\CMS\Version\Dependency\EventCallback::class, + \TYPO3\CMS\Workspaces\Dependency\EventCallback::class, $this->getElementEntityProcessor(), $method, $targetArguments diff --git a/typo3/sysext/version/Classes/Dependency/DependencyEntityFactory.php b/typo3/sysext/workspaces/Classes/Dependency/DependencyEntityFactory.php similarity index 68% rename from typo3/sysext/version/Classes/Dependency/DependencyEntityFactory.php rename to typo3/sysext/workspaces/Classes/Dependency/DependencyEntityFactory.php index 78660e396d367aca72d3fabb74c61339c942d740..95e90ca8d960c44e8a912f89af6b5fb2764f0237 100644 --- a/typo3/sysext/version/Classes/Dependency/DependencyEntityFactory.php +++ b/typo3/sysext/workspaces/Classes/Dependency/DependencyEntityFactory.php @@ -1,5 +1,5 @@ <?php -namespace TYPO3\CMS\Version\Dependency; +namespace TYPO3\CMS\Workspaces\Dependency; /* * This file is part of the TYPO3 CMS project. @@ -35,13 +35,13 @@ class DependencyEntityFactory * @param string $table * @param int $id * @param array $data (optional) - * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency - * @return \TYPO3\CMS\Version\Dependency\ElementEntity + * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency + * @return \TYPO3\CMS\Workspaces\Dependency\ElementEntity */ - public function getElement($table, $id, array $data = [], \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency) + public function getElement($table, $id, array $data = [], \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency) { /** @var $element ElementEntity */ - $element = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Version\Dependency\ElementEntity::class, $table, $id, $data, $dependency); + $element = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Dependency\ElementEntity::class, $table, $id, $data, $dependency); $elementName = $element->__toString(); if (!isset($this->elements[$elementName])) { $this->elements[$elementName] = $element; @@ -52,15 +52,15 @@ class DependencyEntityFactory /** * Gets and registers a new reference. * - * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element + * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $element * @param string $field - * @return \TYPO3\CMS\Version\Dependency\ReferenceEntity + * @return \TYPO3\CMS\Workspaces\Dependency\ReferenceEntity */ - public function getReference(\TYPO3\CMS\Version\Dependency\ElementEntity $element, $field) + public function getReference(\TYPO3\CMS\Workspaces\Dependency\ElementEntity $element, $field) { $referenceName = $element->__toString() . '.' . $field; if (!isset($this->references[$referenceName][$field])) { - $this->references[$referenceName][$field] = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Version\Dependency\ReferenceEntity::class, $element, $field); + $this->references[$referenceName][$field] = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Dependency\ReferenceEntity::class, $element, $field); } return $this->references[$referenceName][$field]; } @@ -72,12 +72,12 @@ class DependencyEntityFactory * @param int $id * @param string $field * @param array $data (optional) - * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency - * @return \TYPO3\CMS\Version\Dependency\ReferenceEntity + * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency + * @return \TYPO3\CMS\Workspaces\Dependency\ReferenceEntity * @see getElement * @see getReference */ - public function getReferencedElement($table, $id, $field, array $data = [], \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency) + public function getReferencedElement($table, $id, $field, array $data = [], \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency) { return $this->getReference($this->getElement($table, $id, $data, $dependency), $field); } diff --git a/typo3/sysext/version/Classes/Dependency/DependencyResolver.php b/typo3/sysext/workspaces/Classes/Dependency/DependencyResolver.php similarity index 80% rename from typo3/sysext/version/Classes/Dependency/DependencyResolver.php rename to typo3/sysext/workspaces/Classes/Dependency/DependencyResolver.php index e3dd2730b180ba82a7099b52ef0be1cdb85688d9..01eaf93afbc31bdba1fe2d8d651af14f5404e798 100644 --- a/typo3/sysext/version/Classes/Dependency/DependencyResolver.php +++ b/typo3/sysext/workspaces/Classes/Dependency/DependencyResolver.php @@ -1,5 +1,5 @@ <?php -namespace TYPO3\CMS\Version\Dependency; +namespace TYPO3\CMS\Workspaces\Dependency; /* * This file is part of the TYPO3 CMS project. @@ -25,7 +25,7 @@ class DependencyResolver protected $workspace = 0; /** - * @var \TYPO3\CMS\Version\Dependency\DependencyEntityFactory + * @var \TYPO3\CMS\Workspaces\Dependency\DependencyEntityFactory */ protected $factory; @@ -73,10 +73,10 @@ class DependencyResolver * Sets a callback for a particular event. * * @param string $eventName - * @param \TYPO3\CMS\Version\Dependency\EventCallback $callback - * @return \TYPO3\CMS\Version\Dependency\DependencyResolver + * @param \TYPO3\CMS\Workspaces\Dependency\EventCallback $callback + * @return \TYPO3\CMS\Workspaces\Dependency\DependencyResolver */ - public function setEventCallback($eventName, \TYPO3\CMS\Version\Dependency\EventCallback $callback) + public function setEventCallback($eventName, \TYPO3\CMS\Workspaces\Dependency\EventCallback $callback) { $this->eventCallbacks[$eventName] = $callback; return $this; @@ -93,7 +93,7 @@ class DependencyResolver public function executeEventCallback($eventName, $caller, array $callerArguments = []) { if (isset($this->eventCallbacks[$eventName])) { - /** @var $callback \TYPO3\CMS\Version\Dependency\EventCallback */ + /** @var $callback \TYPO3\CMS\Workspaces\Dependency\EventCallback */ $callback = $this->eventCallbacks[$eventName]; return $callback->execute($callerArguments, $caller, $eventName); } @@ -104,7 +104,7 @@ class DependencyResolver * Sets the condition that outermost parents required at least one child or parent reference. * * @param bool $outerMostParentsRequireReferences - * @return \TYPO3\CMS\Version\Dependency\DependencyResolver + * @return \TYPO3\CMS\Workspaces\Dependency\DependencyResolver */ public function setOuterMostParentsRequireReferences($outerMostParentsRequireReferences) { @@ -118,7 +118,7 @@ class DependencyResolver * @param string $table * @param int $id * @param array $data - * @return \TYPO3\CMS\Version\Dependency\ElementEntity + * @return \TYPO3\CMS\Workspaces\Dependency\ElementEntity */ public function addElement($table, $id, array $data = []) { @@ -131,13 +131,13 @@ class DependencyResolver /** * Gets the outermost parents that define complete dependent structure each. * - * @return array|\TYPO3\CMS\Version\Dependency\ElementEntity[] + * @return array|\TYPO3\CMS\Workspaces\Dependency\ElementEntity[] */ public function getOuterMostParents() { if (!isset($this->outerMostParents)) { $this->outerMostParents = []; - /** @var $element \TYPO3\CMS\Version\Dependency\ElementEntity */ + /** @var $element \TYPO3\CMS\Workspaces\Dependency\ElementEntity */ foreach ($this->elements as $element) { $this->processOuterMostParent($element); } @@ -148,9 +148,9 @@ class DependencyResolver /** * Processes and registers the outermost parents accordant to the registered elements. * - * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element + * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $element */ - protected function processOuterMostParent(\TYPO3\CMS\Version\Dependency\ElementEntity $element) + protected function processOuterMostParent(\TYPO3\CMS\Workspaces\Dependency\ElementEntity $element) { if ($this->outerMostParentsRequireReferences === false || $element->hasReferences()) { $outerMostParent = $element->getOuterMostParent(); @@ -167,10 +167,10 @@ class DependencyResolver * Gets all nested elements (including the parent) of a particular outermost parent element. * * @throws \RuntimeException - * @param \TYPO3\CMS\Version\Dependency\ElementEntity $outerMostParent + * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $outerMostParent * @return array */ - public function getNestedElements(\TYPO3\CMS\Version\Dependency\ElementEntity $outerMostParent) + public function getNestedElements(\TYPO3\CMS\Workspaces\Dependency\ElementEntity $outerMostParent) { $outerMostParentName = $outerMostParent->__toString(); if (!isset($this->outerMostParents[$outerMostParentName])) { @@ -193,12 +193,12 @@ class DependencyResolver /** * Gets an instance of the factory to keep track of element or reference entities. * - * @return \TYPO3\CMS\Version\Dependency\DependencyEntityFactory + * @return \TYPO3\CMS\Workspaces\Dependency\DependencyEntityFactory */ public function getFactory() { if (!isset($this->factory)) { - $this->factory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Version\Dependency\DependencyEntityFactory::class); + $this->factory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Dependency\DependencyEntityFactory::class); } return $this->factory; } diff --git a/typo3/sysext/version/Classes/Dependency/ElementEntity.php b/typo3/sysext/workspaces/Classes/Dependency/ElementEntity.php similarity index 94% rename from typo3/sysext/version/Classes/Dependency/ElementEntity.php rename to typo3/sysext/workspaces/Classes/Dependency/ElementEntity.php index f6a4811c828a5ca781aea1efb39741d3932b9ba1..b3e40fec9af2f6a410aafb7a80f37da8091507e8 100644 --- a/typo3/sysext/version/Classes/Dependency/ElementEntity.php +++ b/typo3/sysext/workspaces/Classes/Dependency/ElementEntity.php @@ -1,5 +1,5 @@ <?php -namespace TYPO3\CMS\Version\Dependency; +namespace TYPO3\CMS\Workspaces\Dependency; /* * This file is part of the TYPO3 CMS project. @@ -54,7 +54,7 @@ class ElementEntity protected $record; /** - * @var \TYPO3\CMS\Version\Dependency\DependencyResolver + * @var \TYPO3\CMS\Workspaces\Dependency\DependencyResolver */ protected $dependency; @@ -74,7 +74,7 @@ class ElementEntity protected $traversingParents = false; /** - * @var \TYPO3\CMS\Version\Dependency\ElementEntity + * @var \TYPO3\CMS\Workspaces\Dependency\ElementEntity */ protected $outerMostParent; @@ -89,9 +89,9 @@ class ElementEntity * @param string $table * @param int $id * @param array $data (optional) - * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency + * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency */ - public function __construct($table, $id, array $data = [], \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency) + public function __construct($table, $id, array $data = [], \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency) { $this->table = $table; $this->id = (int)$id; @@ -206,7 +206,7 @@ class ElementEntity /** * Gets the parent dependency object. * - * @return \TYPO3\CMS\Version\Dependency\DependencyResolver + * @return \TYPO3\CMS\Workspaces\Dependency\DependencyResolver */ public function getDependency() { @@ -361,10 +361,10 @@ class ElementEntity $this->outerMostParent = $this; } else { $this->outerMostParent = false; - /** @var $parent \TYPO3\CMS\Version\Dependency\ReferenceEntity */ + /** @var $parent \TYPO3\CMS\Workspaces\Dependency\ReferenceEntity */ foreach ($parents as $parent) { $outerMostParent = $parent->getElement()->getOuterMostParent(); - if ($outerMostParent instanceof \TYPO3\CMS\Version\Dependency\ElementEntity) { + if ($outerMostParent instanceof \TYPO3\CMS\Workspaces\Dependency\ElementEntity) { $this->outerMostParent = $outerMostParent; break; } @@ -387,7 +387,7 @@ class ElementEntity if (!isset($this->nestedChildren)) { $this->nestedChildren = []; $children = $this->getChildren(); - /** @var $child \TYPO3\CMS\Version\Dependency\ReferenceEntity */ + /** @var $child \TYPO3\CMS\Workspaces\Dependency\ReferenceEntity */ foreach ($children as $child) { $this->nestedChildren = array_merge($this->nestedChildren, [$child->getElement()->__toString() => $child->getElement()], $child->getElement()->getNestedChildren()); } diff --git a/typo3/sysext/version/Classes/Dependency/ElementEntityProcessor.php b/typo3/sysext/workspaces/Classes/Dependency/ElementEntityProcessor.php similarity index 98% rename from typo3/sysext/version/Classes/Dependency/ElementEntityProcessor.php rename to typo3/sysext/workspaces/Classes/Dependency/ElementEntityProcessor.php index e65a2df12acd69f16462414c1e2787e03fdb472e..01554403817257407cb5ef06e79e62eb51d2de9f 100644 --- a/typo3/sysext/version/Classes/Dependency/ElementEntityProcessor.php +++ b/typo3/sysext/workspaces/Classes/Dependency/ElementEntityProcessor.php @@ -1,5 +1,5 @@ <?php -namespace TYPO3\CMS\Version\Dependency; +namespace TYPO3\CMS\Workspaces\Dependency; /* * This file is part of the TYPO3 CMS project. @@ -105,7 +105,7 @@ class ElementEntityProcessor * * @param array $callerArguments * @param array $targetArgument - * @param \TYPO3\CMS\Version\Dependency\ElementEntity $caller + * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $caller * @param string $eventName * @return NULL|string Skip response (if required) */ diff --git a/typo3/sysext/version/Classes/Dependency/EventCallback.php b/typo3/sysext/workspaces/Classes/Dependency/EventCallback.php similarity index 97% rename from typo3/sysext/version/Classes/Dependency/EventCallback.php rename to typo3/sysext/workspaces/Classes/Dependency/EventCallback.php index 71d7200396078737bb1d9f7e67fb13f09393381a..096fd7495916e3a6ace21dd96b259946ebfa391e 100644 --- a/typo3/sysext/version/Classes/Dependency/EventCallback.php +++ b/typo3/sysext/workspaces/Classes/Dependency/EventCallback.php @@ -1,5 +1,5 @@ <?php -namespace TYPO3\CMS\Version\Dependency; +namespace TYPO3\CMS\Workspaces\Dependency; /* * This file is part of the TYPO3 CMS project. diff --git a/typo3/sysext/version/Classes/Dependency/ReferenceEntity.php b/typo3/sysext/workspaces/Classes/Dependency/ReferenceEntity.php similarity index 79% rename from typo3/sysext/version/Classes/Dependency/ReferenceEntity.php rename to typo3/sysext/workspaces/Classes/Dependency/ReferenceEntity.php index 47864db9cd7bd980529e30d47cdd3a87437b88b6..e2256efa84f65e90fc8c05c96382787ba7ccd584 100644 --- a/typo3/sysext/version/Classes/Dependency/ReferenceEntity.php +++ b/typo3/sysext/workspaces/Classes/Dependency/ReferenceEntity.php @@ -1,5 +1,5 @@ <?php -namespace TYPO3\CMS\Version\Dependency; +namespace TYPO3\CMS\Workspaces\Dependency; /* * This file is part of the TYPO3 CMS project. @@ -20,7 +20,7 @@ namespace TYPO3\CMS\Version\Dependency; class ReferenceEntity { /** - * @var \TYPO3\CMS\Version\Dependency\ElementEntity + * @var \TYPO3\CMS\Workspaces\Dependency\ElementEntity */ protected $element; @@ -32,10 +32,10 @@ class ReferenceEntity /** * Creates this object. * - * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element + * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $element * @param string $field */ - public function __construct(\TYPO3\CMS\Version\Dependency\ElementEntity $element, $field) + public function __construct(\TYPO3\CMS\Workspaces\Dependency\ElementEntity $element, $field) { $this->element = $element; $this->field = $field; @@ -44,7 +44,7 @@ class ReferenceEntity /** * Gets the elements. * - * @return \TYPO3\CMS\Version\Dependency\ElementEntity + * @return \TYPO3\CMS\Workspaces\Dependency\ElementEntity */ public function getElement() { diff --git a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php index a77acd455e7decb00dd68a0c4d1c0204b99f4b88..2819d812f43705483cfbfb801f9cbcdc0da58770 100644 --- a/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php +++ b/typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php @@ -14,18 +14,267 @@ namespace TYPO3\CMS\Workspaces\Hook; * The TYPO3 project - inspiring people to share! */ +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Platforms\SQLServerPlatform; use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction; use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; +use TYPO3\CMS\Core\Database\ReferenceIndex; +use TYPO3\CMS\Core\DataHandling\DataHandler; +use TYPO3\CMS\Core\Localization\LanguageService; +use TYPO3\CMS\Core\Service\MarkerBasedTemplateService; +use TYPO3\CMS\Core\Utility\ArrayUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Versioning\VersionState; use TYPO3\CMS\Workspaces\Service\StagesService; /** - * DataHandler service + * Contains some parts for staging, versioning and workspaces + * to interact with the TYPO3 Core Engine */ class DataHandlerHook { + /** + * For accumulating information about workspace stages raised + * on elements so a single mail is sent as notification. + * previously called "accumulateForNotifEmail" in DataHandler + * + * @var array + */ + protected $notificationEmailInfo = []; + + /** + * Contains remapped IDs. + * + * @var array + */ + protected $remappedIds = []; + + /** + * @var \TYPO3\CMS\Workspaces\Service\WorkspaceService + */ + protected $workspaceService; + + /**************************** + ***** Cmdmap Hooks ****** + ****************************/ + /** + * hook that is called before any cmd of the commandmap is executed + * + * @param DataHandler $dataHandler reference to the main DataHandler object + */ + public function processCmdmap_beforeStart(DataHandler $dataHandler) + { + // Reset notification array + $this->notificationEmailInfo = []; + // Resolve dependencies of version/workspaces actions: + $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get(); + } + + /** + * hook that is called when no prepared command was found + * + * @param string $command the command to be executed + * @param string $table the table of the record + * @param int $id the ID of the record + * @param mixed $value the value containing the data + * @param bool $commandIsProcessed can be set so that other hooks or + * @param DataHandler $dataHandler reference to the main DataHandler object + */ + public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler) + { + // custom command "version" + if ($command === 'version') { + $commandIsProcessed = true; + $action = (string)$value['action']; + $comment = !empty($value['comment']) ? $value['comment'] : ''; + $notificationAlternativeRecipients = (isset($value['notificationAlternativeRecipients'])) && is_array($value['notificationAlternativeRecipients']) ? $value['notificationAlternativeRecipients'] : []; + switch ($action) { + case 'new': + $dataHandler->versionizeRecord($table, $id, $value['label']); + break; + case 'swap': + $this->version_swap( + $table, + $id, + $value['swapWith'], + $value['swapIntoWS'], + $dataHandler, + $comment, + true, + $notificationAlternativeRecipients + ); + break; + case 'clearWSID': + $this->version_clearWSID($table, $id, false, $dataHandler); + break; + case 'flush': + $this->version_clearWSID($table, $id, true, $dataHandler); + break; + case 'setStage': + $elementIds = GeneralUtility::trimExplode(',', $id, true); + foreach ($elementIds as $elementId) { + $this->version_setStage( + $table, + $elementId, + $value['stageId'], + $comment, + true, + $dataHandler, + $notificationAlternativeRecipients + ); + } + break; + default: + // Do nothing + } + } + } + + /** + * hook that is called AFTER all commands of the commandmap was + * executed + * + * @param DataHandler $dataHandler reference to the main DataHandler object + */ + public function processCmdmap_afterFinish(DataHandler $dataHandler) + { + // Empty accumulation array: + foreach ($this->notificationEmailInfo as $notifItem) { + $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $dataHandler, $notifItem['alternativeRecipients']); + } + // Reset notification array + $this->notificationEmailInfo = []; + // Reset remapped IDs + $this->remappedIds = []; + + $this->flushWorkspaceCacheEntriesByWorkspaceId($dataHandler->BE_USER->workspace); + } + + /** + * hook that is called when an element shall get deleted + * + * @param string $table the table of the record + * @param int $id the ID of the record + * @param array $record The accordant database record + * @param bool $recordWasDeleted can be set so that other hooks or + * @param DataHandler $dataHandler reference to the main DataHandler object + */ + public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler) + { + // only process the hook if it wasn't processed + // by someone else before + if ($recordWasDeleted) { + return; + } + $recordWasDeleted = true; + // For Live version, try if there is a workspace version because if so, rather "delete" that instead + // Look, if record is an offline version, then delete directly: + if ($record['pid'] != -1) { + if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) { + $record = $wsVersion; + $id = $record['uid']; + } + } + $recordVersionState = VersionState::cast($record['t3ver_state']); + // Look, if record is an offline version, then delete directly: + if ($record['pid'] == -1) { + if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { + // In Live workspace, delete any. In other workspaces there must be match. + if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) { + $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state'); + // Processing can be skipped if a delete placeholder shall be swapped/published + // during the current request. Thus it will be deleted later on... + $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']); + if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid']) + && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action']) + && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith']) + && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap' + && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id + ) { + return null; + } + + if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) { + // Change normal versioned record to delete placeholder + // Happens when an edited record is deleted + GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable($table) + ->update( + $table, + [ + 't3ver_label' => 'DELETED!', + 't3ver_state' => 2, + ], + ['uid' => $id] + ); + + // Delete localization overlays: + $dataHandler->deleteL10nOverlayRecords($table, $id); + } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) { + // Delete those in WS 0 + if their live records state was not "Placeholder". + $dataHandler->deleteEl($table, $id); + // Delete move-placeholder if current version record is a move-to-pointer + if ($recordVersionState->equals(VersionState::MOVE_POINTER)) { + $movePlaceholder = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid', $record['t3ver_wsid']); + if (!empty($movePlaceholder)) { + $dataHandler->deleteEl($table, $movePlaceholder['uid']); + } + } + } else { + // If live record was placeholder (new/deleted), rather clear + // it from workspace (because it clears both version and placeholder). + $this->version_clearWSID($table, $id, false, $dataHandler); + } + } else { + $dataHandler->newlog('Tried to delete record from another workspace', 1); + } + } else { + $dataHandler->newlog('Versioning not enabled for record with PID = -1!', 2); + } + } elseif ($res = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($record['pid'], $table)) { + // Look, if record is "online" or in a versionized branch, then delete directly. + if ($res > 0) { + $dataHandler->deleteEl($table, $id); + } else { + $dataHandler->newlog('Stage of root point did not allow for deletion', 1); + } + } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) { + // Placeholders for moving operations are deletable directly. + // Get record which its a placeholder for and reset the t3ver_state of that: + if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) { + // Clear the state flag of the workspace version of the record + // Setting placeholder state value for version (so it can know it is currently a new version...) + + GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable($table) + ->update( + $table, + [ + 't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE) + ], + ['uid' => (int)$wsRec['uid']] + ); + } + $dataHandler->deleteEl($table, $id); + } else { + // Otherwise, try to delete by versioning: + $copyMappingArray = $dataHandler->copyMappingArray; + $dataHandler->versionizeRecord($table, $id, 'DELETED!', true); + // Determine newly created versions: + // (remove placeholders are copied and modified, thus they appear in the copyMappingArray) + $versionizedElements = ArrayUtility::arrayDiffAssocRecursive($dataHandler->copyMappingArray, $copyMappingArray); + // Delete localization overlays: + foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) { + foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) { + $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId); + } + } + } + } + /** * In case a sys_workspace_stage record is deleted we do a hard reset * for all existing records in that stage to avoid that any of these end up @@ -49,14 +298,926 @@ class DataHandlerHook } /** - * hook that is called AFTER all commands of the commandmap was - * executed + * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about + * moving records that are *not* in the live workspace * - * @param \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler reference to the main DataHandler object + * @param string $table the table of the record + * @param int $uid the ID of the record + * @param int $destPid Position to move to: $destPid: >=0 then it points to + * @param array $propArr Record properties, like header and pid (includes workspace overlay) + * @param array $moveRec Record properties, like header and pid (without workspace overlay) + * @param int $resolvedPid The final page ID of the record + * @param bool $recordWasMoved can be set so that other hooks or + * @param DataHandler $dataHandler */ - public function processCmdmap_afterFinish(\TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler) + public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler) { - $this->flushWorkspaceCacheEntriesByWorkspaceId($dataHandler->BE_USER->workspace); + // Only do something in Draft workspace + if ($dataHandler->BE_USER->workspace === 0) { + return; + } + if ($destPid < 0) { + // Fetch move placeholder, since it might point to a new page in the current workspace + $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid'); + if ($movePlaceHolder !== false) { + $resolvedPid = $movePlaceHolder['pid']; + } + } + $recordWasMoved = true; + $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']); + // Get workspace version of the source record, if any: + $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid'); + // Handle move-placeholders if the current record is not one already + if ( + BackendUtility::isTableWorkspaceEnabled($table) + && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER) + ) { + // Create version of record first, if it does not exist + if (empty($WSversion['uid'])) { + $dataHandler->versionizeRecord($table, $uid, 'MovePointer'); + $WSversion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid'); + $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid); + } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) { + // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders + $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid); + } + } + // Check workspace permissions: + $workspaceAccessBlocked = []; + // Element was in "New/Deleted/Moved" so it can be moved... + $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder(); + $destRes = $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table); + $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableWorkspaceEnabled($table)); + // Workspace source check: + if (!$recIsNewVersion) { + $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid); + if ($errorCode) { + $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' '; + } elseif (!$canMoveRecord && $dataHandler->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) { + $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" '; + } + } + // Workspace destination check: + // All records can be inserted if $destRes is greater than zero. + // Only new versions can be inserted if $destRes is FALSE. + // NO RECORDS can be inserted if $destRes is negative which indicates a stage + // not allowed for use. If "versioningWS" is version 2, moving can take place of versions. + // since TYPO3 CMS 7, version2 is the default and the only option + if (!($destRes > 0 || $canMoveRecord && !$destRes)) { + $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" '; + } elseif ($destRes == 1 && $WSversion['uid']) { + $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID '; + } + if (empty($workspaceAccessBlocked)) { + // If the move operation is done on a versioned record, which is + // NOT new/deleted placeholder and versioningWS is in version 2, then... + // since TYPO3 CMS 7, version2 is the default and the only option + if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableWorkspaceEnabled($table)) { + $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $dataHandler); + } else { + // moving not needed, just behave like in live workspace + $recordWasMoved = false; + } + } else { + $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1); + } + } + + /** + * Processes fields of a moved record and follows references. + * + * @param DataHandler $dataHandler Calling DataHandler instance + * @param int $resolvedPageId Resolved real destination page id + * @param string $table Name of parent table + * @param int $uid UID of the parent record + */ + protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid) + { + $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid); + if (empty($versionedRecord)) { + return; + } + foreach ($versionedRecord as $field => $value) { + if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) { + continue; + } + $this->moveRecord_processFieldValue( + $dataHandler, + $resolvedPageId, + $table, + $uid, + $field, + $value, + $GLOBALS['TCA'][$table]['columns'][$field]['config'] + ); + } + } + + /** + * Processes a single field of a moved record and follows references. + * + * @param DataHandler $dataHandler Calling DataHandler instance + * @param int $resolvedPageId Resolved real destination page id + * @param string $table Name of parent table + * @param int $uid UID of the parent record + * @param string $field Name of the field of the parent record + * @param string $value Value of the field of the parent record + * @param array $configuration TCA field configuration of the parent record + */ + protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration) + { + $inlineFieldType = $dataHandler->getInlineFieldType($configuration); + $inlineProcessing = ( + ($inlineFieldType === 'list' || $inlineFieldType === 'field') + && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table']) + && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent']) + ); + + if ($inlineProcessing) { + if ($table === 'pages') { + // If the inline elements are related to a page record, + // make sure they reside at that page and not at its parent + $resolvedPageId = $uid; + } + + $dbAnalysis = $this->createRelationHandlerInstance(); + $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration); + + // Moving records to a positive destination will insert each + // record at the beginning, thus the order is reversed here: + foreach ($dbAnalysis->itemArray as $item) { + $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state'); + if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) { + continue; + } + $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId); + } + } + } + + /**************************** + ***** Notifications ****** + ****************************/ + /** + * Send an email notification to users in workspace + * + * @param array $stat Workspace access array from \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::checkWorkspace() + * @param int $stageId New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected! + * @param string $table Table name of element (or list of element names if $id is zero) + * @param int $id Record uid of element (if zero, then $table is used as reference to element(s) alone) + * @param string $comment User comment sent along with action + * @param DataHandler $dataHandler DataHandler object + * @param array $notificationAlternativeRecipients List of recipients to notify instead of be_users selected by sys_workspace, list is generated by workspace extension module + */ + protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = []) + { + $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']); + // So, if $id is not set, then $table is taken to be the complete element name! + $elementName = $id ? $table . ':' . $id : $table; + if (!is_array($workspaceRec)) { + return; + } + + // Get the new stage title + $stageService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class); + $newStage = $stageService->getStageTitle((int)$stageId); + if (empty($notificationAlternativeRecipients)) { + // Compile list of recipients: + $emails = []; + switch ((int)$stat['stagechg_notification']) { + case 1: + switch ((int)$stageId) { + case 1: + $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']); + break; + case 10: + $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); + break; + case -1: + // List of elements to reject: + $allElements = explode(',', $elementName); + // Traverse them, and find the history of each + foreach ($allElements as $elRef) { + list($eTable, $eUid) = explode(':', $elRef); + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable('sys_log'); + + $queryBuilder->getRestrictions()->removeAll(); + + $result = $queryBuilder + ->select('log_data', 'tstamp', 'userid') + ->from('sys_log') + ->where( + $queryBuilder->expr()->eq( + 'action', + $queryBuilder->createNamedParameter(6, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'details_nr', + $queryBuilder->createNamedParameter(30, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'tablename', + $queryBuilder->createNamedParameter($eTable, \PDO::PARAM_STR) + ), + $queryBuilder->expr()->eq( + 'recuid', + $queryBuilder->createNamedParameter($eUid, \PDO::PARAM_INT) + ) + ) + ->orderBy('uid', 'DESC') + ->execute(); + + // Find all implicated since the last stage-raise from editing to review: + while ($dat = $result->fetch()) { + $data = unserialize($dat['log_data']); + $emails = $this->getEmailsForStageChangeNotification($dat['userid'], true) + $emails; + if ($data['stage'] == 1) { + break; + } + } + } + break; + case 0: + $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']); + break; + default: + $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); + } + break; + case 10: + $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], true); + $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails; + $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails; + break; + default: + // Do nothing + } + } else { + $emails = $notificationAlternativeRecipients; + } + // prepare and then send the emails + if (!empty($emails)) { + // Path to record is found: + list($elementTable, $elementUid) = explode(':', $elementName); + $elementUid = (int)$elementUid; + $elementRecord = BackendUtility::getRecord($elementTable, $elementUid); + $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord); + if ($elementTable === 'pages') { + $pageUid = $elementUid; + } else { + BackendUtility::fixVersioningPid($elementTable, $elementRecord); + $pageUid = ($elementUid = $elementRecord['pid']); + } + + // new way, options are + // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject + // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject + $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid); + $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.']; + $markers = [ + '###RECORD_TITLE###' => $recordTitle, + '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20), + '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'], + '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir, + '###WORKSPACE_TITLE###' => $workspaceRec['title'], + '###WORKSPACE_UID###' => $workspaceRec['uid'], + '###ELEMENT_NAME###' => $elementName, + '###NEXT_STAGE###' => $newStage, + '###COMMENT###' => $comment, + // See: #30212 - keep both markers for compatibility + '###USER_REALNAME###' => $dataHandler->BE_USER->user['realName'], + '###USER_FULLNAME###' => $dataHandler->BE_USER->user['realName'], + '###USER_USERNAME###' => $dataHandler->BE_USER->user['username'] + ]; + // add marker for preview links if workspace extension is loaded + $this->workspaceService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class); + // only generate the link if the marker is in the template - prevents database from getting to much entries + if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) { + $tempEmailMessage = $this->getLanguageService()->sL($emailConfig['message']); + } else { + $tempEmailMessage = $emailConfig['message']; + } + if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== false) { + $markers['###PREVIEW_LINK###'] = $this->workspaceService->generateWorkspacePreviewLink($elementUid); + } + unset($tempEmailMessage); + $markers['###SPLITTED_PREVIEW_LINK###'] = $this->workspaceService->generateWorkspaceSplittedPreviewLink($elementUid, true); + // Hook for preprocessing of the content for formmails: + if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'])) { + foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] as $className) { + $_procObj = GeneralUtility::makeInstance($className); + $markers = $_procObj->postModifyMarkers($markers, $this); + } + } + // send an email to each individual user, to ensure the + // multilanguage version of the email + $emailRecipients = []; + // an array of language objects that are needed + // for emails with different languages + $languageObjects = [ + $this->getLanguageService()->lang => $this->getLanguageService() + ]; + // loop through each recipient and send the email + foreach ($emails as $recipientData) { + // don't send an email twice + if (isset($emailRecipients[$recipientData['email']])) { + continue; + } + $emailSubject = $emailConfig['subject']; + $emailMessage = $emailConfig['message']; + $emailRecipients[$recipientData['email']] = $recipientData['email']; + // check if the email needs to be localized + // in the users' language + if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) { + $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default'; + if (!isset($languageObjects[$recipientLanguage])) { + // a LANG object in this language hasn't been + // instantiated yet, so this is done here + /** @var $languageObject \TYPO3\CMS\Core\Localization\LanguageService */ + $languageObject = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Localization\LanguageService::class); + $languageObject->init($recipientLanguage); + $languageObjects[$recipientLanguage] = $languageObject; + } else { + $languageObject = $languageObjects[$recipientLanguage]; + } + if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) { + $emailSubject = $languageObject->sL($emailSubject); + } + if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) { + $emailMessage = $languageObject->sL($emailMessage); + } + } + $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class); + $emailSubject = $templateService->substituteMarkerArray($emailSubject, $markers, '', true, true); + $emailMessage = $templateService->substituteMarkerArray($emailMessage, $markers, '', true, true); + // Send an email to the recipient + /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */ + $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class); + if (!empty($recipientData['realName'])) { + $recipient = [$recipientData['email'] => $recipientData['realName']]; + } else { + $recipient = $recipientData['email']; + } + $mail->setTo($recipient) + ->setSubject($emailSubject) + ->setBody($emailMessage); + $mail->send(); + } + $emailRecipients = implode(',', $emailRecipients); + $dataHandler->newlog2('Notification email for stage change was sent to "' . $emailRecipients . '"', $table, $id); + } + } + + /** + * Return be_users that should be notified on stage change from input list. + * previously called notifyStageChange_getEmails() in DataHandler + * + * @param string $listOfUsers List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set. + * @param bool $noTablePrefix If set, the input list are integers and not strings. + * @return array Array of emails + */ + protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = false) + { + $users = GeneralUtility::trimExplode(',', $listOfUsers, true); + $emails = []; + foreach ($users as $userIdent) { + if ($noTablePrefix) { + $id = (int)$userIdent; + } else { + list($table, $id) = GeneralUtility::revExplode('_', $userIdent, 2); + } + if ($table === 'be_users' || $noTablePrefix) { + if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) { + if (trim($userRecord['email']) !== '') { + $emails[$id] = $userRecord; + } + } + } + } + return $emails; + } + + /**************************** + ***** Stage Changes ****** + ****************************/ + /** + * Setting stage of record + * + * @param string $table Table name + * @param int $integer Record UID + * @param int $stageId Stage ID to set + * @param string $comment Comment that goes into log + * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email? + * @param DataHandler $dataHandler DataHandler object + * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users + */ + protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = false, DataHandler $dataHandler, array $notificationAlternativeRecipients = []) + { + if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) { + $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, 1); + } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) { + $record = BackendUtility::getRecord($table, $id); + $stat = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']); + // check if the usere is allowed to the current stage, so it's also allowed to send to next stage + if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) { + // Set stage of record: + GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable($table) + ->update( + $table, + [ + 't3ver_stage' => $stageId, + ], + ['uid' => (int)$id] + ); + $dataHandler->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id); + // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere! + $dataHandler->log($table, $id, 6, 0, 0, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]); + if ((int)$stat['stagechg_notification'] > 0) { + if ($notificationEmailInfo) { + $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$stat, $stageId, $comment]; + $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id; + $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients; + } else { + $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $dataHandler, $notificationAlternativeRecipients); + } + } + } else { + $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1); + } + } else { + $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', 1); + } + } + + /***************************** + ***** CMD versioning ****** + *****************************/ + + /** + * 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 + */ + 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); + + $platform = $connection->getDatabasePlatform(); + $tableDetails = null; + if ($platform instanceof SQLServerPlatform) { + // mssql needs to set proper PARAM_LOB and others to update fields + $tableDetails = $connection->getSchemaManager()->listTableDetails($table); + } + + try { + $types = []; + + if ($platform instanceof SQLServerPlatform) { + foreach ($curVersion as $columnName => $columnValue) { + $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType(); + } + } + + $connection->update( + $table, + $swapVersion, + ['uid' => (int)$id], + $types + ); + } catch (DBALException $e) { + $sqlErrors[] = $e->getPrevious()->getMessage(); + } + + if (empty($sqlErrors)) { + try { + $types = []; + if ($platform instanceof SQLServerPlatform) { + foreach ($curVersion as $columnName => $columnValue) { + $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType(); + } + } + + $connection->update( + $table, + $curVersion, + ['uid' => (int)$swapWith], + $types + ); + unlink($lockFileName); + } catch (DBALException $e) { + $sqlErrors[] = $e->getPrevious()->getMessage(); + } + } + + 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); + } + } + + /** + * Writes remapped foreign field (IRRE). + * + * @param \TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis Instance that holds the sorting order of child records + * @param array $configuration The TCA field configuration + * @param int $parentId The uid of the parent record + */ + public function writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId) + { + foreach ($dbAnalysis->itemArray as &$item) { + if (isset($this->remappedIds[$item['table']][$item['id']])) { + $item['id'] = $this->remappedIds[$item['table']][$item['id']]; + } + } + $dbAnalysis->writeForeignField($configuration, $parentId); + } + + /** + * Processes fields of a record for the publishing/swapping process. + * Basically this takes care of IRRE (type "inline") child references. + * + * @param string $tableName Table name + * @param string $fieldName: Field name + * @param array $configuration TCA field configuration + * @param array $liveData: Live record data + * @param array $versionData: Version record data + * @param DataHandler $dataHandler Calling data-handler object + */ + protected function version_swap_processFields($tableName, $fieldName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler) + { + $inlineType = $dataHandler->getInlineFieldType($configuration); + if ($inlineType !== 'field') { + return; + } + $foreignTable = $configuration['foreign_table']; + // Read relations that point to the current record (e.g. live record): + $liveRelations = $this->createRelationHandlerInstance(); + $liveRelations->setWorkspaceId(0); + $liveRelations->start('', $foreignTable, '', $liveData['uid'], $tableName, $configuration); + // Read relations that point to the record to be swapped with e.g. draft record): + $versionRelations = $this->createRelationHandlerInstance(); + $versionRelations->setUseLiveReferenceIds(false); + $versionRelations->start('', $foreignTable, '', $versionData['uid'], $tableName, $configuration); + // Update relations for both (workspace/versioning) sites: + if (count($liveRelations->itemArray)) { + $dataHandler->addRemapAction( + $tableName, + $liveData['uid'], + [$this, 'updateInlineForeignFieldSorting'], + [$tableName, $liveData['uid'], $foreignTable, $liveRelations->tableArray[$foreignTable], $configuration, $dataHandler->BE_USER->workspace] + ); + } + if (count($versionRelations->itemArray)) { + $dataHandler->addRemapAction( + $tableName, + $liveData['uid'], + [$this, 'updateInlineForeignFieldSorting'], + [$tableName, $liveData['uid'], $foreignTable, $versionRelations->tableArray[$foreignTable], $configuration, 0] + ); + } + } + + /** + * Updates foreign field sorting values of versioned and live + * parents after(!) the whole structure has been published. + * + * This method is used as callback function in + * DataHandlerHook::version_swap_procBasedOnFieldType(). + * Sorting fields ("sortby") are not modified during the + * workspace publishing/swapping process directly. + * + * @param string $parentTableName + * @param string $parentId + * @param string $foreignTableName + * @param int[] $foreignIds + * @param array $configuration + * @param int $targetWorkspaceId + * @internal + */ + public function updateInlineForeignFieldSorting($parentTableName, $parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId) + { + $remappedIds = []; + // Use remapped ids (live id <-> version id) + foreach ($foreignIds as $foreignId) { + if (!empty($this->remappedIds[$foreignTableName][$foreignId])) { + $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId]; + } else { + $remappedIds[] = $foreignId; + } + } + + $relationHandler = $this->createRelationHandlerInstance(); + $relationHandler->setWorkspaceId($targetWorkspaceId); + $relationHandler->setUseLiveReferenceIds(false); + $relationHandler->start(implode(',', $remappedIds), $foreignTableName); + $relationHandler->processDeletePlaceholder(); + $relationHandler->writeForeignField($configuration, $parentId); + } + + /** + * Release version from this workspace (and into "Live" workspace but as an offline version). + * + * @param string $table Table name + * @param int $id Record UID + * @param bool $flush If set, will completely delete element + * @param DataHandler $dataHandler DataHandler object + */ + protected function version_clearWSID($table, $id, $flush = false, DataHandler $dataHandler) + { + if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) { + $dataHandler->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1); + return; + } + if (!$dataHandler->checkRecordUpdateAccess($table, $id)) { + $dataHandler->newlog('Attempt to reset workspace for record failed because you do not have edit access', 1); + return; + } + $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state'); + if (!$liveRec) { + return; + } + // Clear workspace ID: + $updateData = [ + 't3ver_wsid' => 0, + 't3ver_tstamp' => $GLOBALS['EXEC_TIME'] + ]; + $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); + $connection->update( + $table, + $updateData, + ['uid' => (int)$id] + ); + + // Clear workspace ID for live version AND DELETE IT as well because it is a new record! + if ( + VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER) + || VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER) + ) { + $connection->update( + $table, + $updateData, + ['uid' => (int)$liveRec['uid']] + ); + + // THIS assumes that the record was placeholder ONLY for ONE record (namely $id) + $dataHandler->deleteEl($table, $liveRec['uid'], true); + } + // If "deleted" flag is set for the version that got released + // it doesn't make sense to keep that "placeholder" anymore and we delete it completly. + $wsRec = BackendUtility::getRecord($table, $id); + if ( + $flush + || ( + VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER) + || VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER) + ) + ) { + $dataHandler->deleteEl($table, $id, true, true); + } + // Remove the move-placeholder if found for live record. + if (BackendUtility::isTableWorkspaceEnabled($table)) { + if ($plhRec = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid')) { + $dataHandler->deleteEl($table, $plhRec['uid'], true, true); + } + } } /** @@ -159,4 +1320,376 @@ class DataHandlerHook $workspacesCache->flushByTag($workspaceId); $workspacesCache->flushByTag(\TYPO3\CMS\Workspaces\Service\WorkspaceService::SELECT_ALL_WORKSPACES); } + + /******************************* + ***** helper functions ****** + *******************************/ + + /** + * Finds all elements for swapping versions in workspace + * + * @param string $table Table name of the original element to swap + * @param int $id UID of the original element to swap (online) + * @param int $offlineId As above but offline + * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID + */ + public function findPageElementsForVersionSwap($table, $id, $offlineId) + { + $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid'); + $workspaceId = (int)$rec['t3ver_wsid']; + $elementData = []; + if ($workspaceId === 0) { + return $elementData; + } + // Get page UID for LIVE and workspace + if ($table !== 'pages') { + $rec = BackendUtility::getRecord($table, $id, 'pid'); + $pageId = $rec['pid']; + $rec = BackendUtility::getRecord('pages', $pageId); + BackendUtility::workspaceOL('pages', $rec, $workspaceId); + $offlinePageId = $rec['_ORIG_uid']; + } else { + $pageId = $id; + $offlinePageId = $offlineId; + } + // Traversing all tables supporting versioning: + foreach ($GLOBALS['TCA'] as $table => $cfg) { + if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable($table); + + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + + $statement = $queryBuilder + ->select('A.uid AS offlineUid', 'B.uid AS uid') + ->from($table, 'A') + ->from($table, 'B') + ->where( + $queryBuilder->expr()->eq( + 'A.pid', + $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'B.pid', + $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'A.t3ver_wsid', + $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) + ) + ->execute(); + + while ($row = $statement->fetch()) { + $elementData[$table][] = [$row['uid'], $row['offlineUid']]; + } + } + } + if ($offlinePageId && $offlinePageId != $pageId) { + $elementData['pages'][] = [$pageId, $offlinePageId]; + } + + return $elementData; + } + + /** + * Searches for all elements from all tables on the given pages in the same workspace. + * + * @param array $pageIdList List of PIDs to search + * @param int $workspaceId Workspace ID + * @param array $elementList List of found elements. Key is table name, value is array of element UIDs + */ + public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList) + { + if ($workspaceId == 0) { + return; + } + // Traversing all tables supporting versioning: + foreach ($GLOBALS['TCA'] as $table => $cfg) { + if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') { + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable($table); + + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + + $statement = $queryBuilder + ->select('A.uid') + ->from($table, 'A') + ->from($table, 'B') + ->where( + $queryBuilder->expr()->eq( + 'A.pid', + $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->in( + 'B.pid', + $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY) + ), + $queryBuilder->expr()->eq( + 'A.t3ver_wsid', + $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) + ) + ->groupBy('A.uid') + ->execute(); + + while ($row = $statement->fetch()) { + $elementList[$table][] = $row['uid']; + } + if (is_array($elementList[$table])) { + // Yes, it is possible to get non-unique array even with DISTINCT above! + // It happens because several UIDs are passed in the array already. + $elementList[$table] = array_unique($elementList[$table]); + } + } + } + } + + /** + * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code> + * + * @param string $table Table to search + * @param array $idList List of records' UIDs + * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publisg DRAFT from ws module! + * @param array $pageIdList List of found page UIDs + * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs + */ + public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList) + { + if ($workspaceId == 0) { + return; + } + + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) + ->getQueryBuilderForTable($table); + $queryBuilder->getRestrictions() + ->removeAll() + ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); + + $statement = $queryBuilder + ->select('B.pid') + ->from($table, 'A') + ->from($table, 'B') + ->where( + $queryBuilder->expr()->eq( + 'A.pid', + $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->eq( + 'A.t3ver_wsid', + $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) + ), + $queryBuilder->expr()->in( + 'A.uid', + $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY) + ), + $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) + ) + ->groupBy('B.pid') + ->execute(); + + while ($row = $statement->fetch()) { + $pageIdList[] = $row['pid']; + // Find ws version + // Note: cannot use BackendUtility::getRecordWSOL() + // here because it does not accept workspace id! + $rec = BackendUtility::getRecord('pages', $row[0]); + BackendUtility::workspaceOL('pages', $rec, $workspaceId); + if ($rec['_ORIG_uid']) { + $elementList['pages'][$row[0]] = $rec['_ORIG_uid']; + } + } + // The line below is necessary even with DISTINCT + // because several elements can be passed by caller + $pageIdList = array_unique($pageIdList); + } + + /** + * Finds real page IDs for state change. + * + * @param array $idList List of page UIDs, possibly versioned + */ + public function findRealPageIds(array &$idList) + { + foreach ($idList as $key => $id) { + $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid'); + if ($rec['t3ver_oid'] > 0) { + $idList[$key] = $rec['t3ver_oid']; + } + } + } + + /** + * Creates a move placeholder for workspaces. + * USE ONLY INTERNALLY + * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER + * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace. + * + * @param string $table Table name to move + * @param int $uid Record uid to move (online record) + * @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 + * @param int $wsUid UID of offline version of online record + * @param DataHandler $dataHandler DataHandler object + * @see moveRecord() + */ + protected function moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $dataHandler) + { + // If a record gets moved after a record that already has a placeholder record + // then the new placeholder record needs to be after the existing one + $originalRecordDestinationPid = $destPid; + if ($destPid < 0) { + $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid'); + if ($movePlaceHolder !== false) { + $destPid = -$movePlaceHolder['uid']; + } + } + if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) { + // If already a placeholder exists, move it: + $dataHandler->moveRecord_raw($table, $plh['uid'], $destPid); + } else { + // First, we create a placeholder record in the Live workspace that + // represents the position to where the record is eventually moved to. + $newVersion_placeholderFieldArray = []; + + // Use property for move placeholders if set (since TYPO3 CMS 6.2) + if (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'])) { + $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders']; + } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'])) { + // Fallback to property for new placeholder (existed long time before TYPO3 CMS 6.2) + $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders']; + } + + // Set values from the versioned record to the move placeholder + if (!empty($shadowColumnsForMovePlaceholder)) { + $versionedRecord = BackendUtility::getRecord($table, $wsUid); + $shadowColumns = GeneralUtility::trimExplode(',', $shadowColumnsForMovePlaceholder, true); + foreach ($shadowColumns as $shadowColumn) { + if (isset($versionedRecord[$shadowColumn])) { + $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn]; + } + } + } + + if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) { + $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME']; + } + if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) { + $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $dataHandler->userid; + } + if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) { + $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME']; + } + if ($table === 'pages') { + // Copy page access settings from original page to placeholder + $perms_clause = $dataHandler->BE_USER->getPagePermsClause(1); + $access = BackendUtility::readPageAccess($uid, $perms_clause); + $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid']; + $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid']; + $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user']; + $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group']; + $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody']; + } + $newVersion_placeholderFieldArray['t3ver_label'] = 'MovePlaceholder #' . $uid; + $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid; + // Setting placeholder state value for temporary record + $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER); + // Setting workspace - only so display of place holders can filter out those from other workspaces. + $newVersion_placeholderFieldArray['t3ver_wsid'] = $dataHandler->BE_USER->workspace; + $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = $dataHandler->getPlaceholderTitleForTableLabel($table, 'MOVE-TO PLACEHOLDER for #' . $uid); + // moving localized records requires to keep localization-settings for the placeholder too + if (isset($GLOBALS['TCA'][$table]['ctrl']['languageField']) && isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'])) { + $l10nParentRec = BackendUtility::getRecord($table, $uid); + $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']]; + $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']]; + if (isset($GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField'])) { + $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]; + } + unset($l10nParentRec); + } + // Initially, create at root level. + $newVersion_placeholderFieldArray['pid'] = 0; + $id = 'NEW_MOVE_PLH'; + // Saving placeholder as 'original' + $dataHandler->insertDB($table, $id, $newVersion_placeholderFieldArray, false); + // Move the new placeholder from temporary root-level to location: + $dataHandler->moveRecord_raw($table, $dataHandler->substNEWwithIDs[$id], $destPid); + // Move the workspace-version of the original to be the version of the move-to-placeholder: + // Setting placeholder state value for version (so it can know it is currently a new version...) + $updateFields = [ + 't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER) + ]; + + GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable($table) + ->update( + $table, + $updateFields, + ['uid' => (int)$wsUid] + ); + } + // Check for the localizations of that element and move them as well + $dataHandler->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid); + } + + /** + * Gets an instance of the command map helper. + * + * @param DataHandler $dataHandler DataHandler object + * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap + */ + public function getCommandMap(DataHandler $dataHandler) + { + return GeneralUtility::makeInstance( + \TYPO3\CMS\Workspaces\DataHandler\CommandMap::class, + $this, + $dataHandler, + $dataHandler->cmdmap, + $dataHandler->BE_USER->workspace + ); + } + + /** + * Returns all fieldnames from a table which have the unique evaluation type set. + * + * @param string $table Table name + * @return array Array of fieldnames + */ + protected function getUniqueFields($table) + { + $listArr = []; + if (empty($GLOBALS['TCA'][$table]['columns'])) { + return $listArr; + } + foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) { + if ($configArr['config']['type'] === 'input') { + $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], true); + if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) { + $listArr[] = $field; + } + } + } + return $listArr; + } + + /** + * @return \TYPO3\CMS\Core\Database\RelationHandler + */ + protected function createRelationHandlerInstance() + { + return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class); + } + + /** + * @return LanguageService + */ + protected function getLanguageService() + { + return $GLOBALS['LANG']; + } } diff --git a/typo3/sysext/workspaces/Classes/Service/Dependency/CollectionService.php b/typo3/sysext/workspaces/Classes/Service/Dependency/CollectionService.php index d376771d9cf2b9362e39774fd377d585584ff9be..86e78d6d58625c1bd5d8e886a6c758a4d82c336b 100644 --- a/typo3/sysext/workspaces/Classes/Service/Dependency/CollectionService.php +++ b/typo3/sysext/workspaces/Classes/Service/Dependency/CollectionService.php @@ -15,7 +15,7 @@ namespace TYPO3\CMS\Workspaces\Service\Dependency; */ use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Version\Dependency; +use TYPO3\CMS\Workspaces\Dependency; use TYPO3\CMS\Workspaces\Service\GridDataService; /** @@ -54,7 +54,7 @@ class CollectionService implements \TYPO3\CMS\Core\SingletonInterface public function getDependencyResolver() { if (!isset($this->dependencyResolver)) { - $this->dependencyResolver = GeneralUtility::makeInstance(\TYPO3\CMS\Version\Dependency\DependencyResolver::class); + $this->dependencyResolver = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Dependency\DependencyResolver::class); $this->dependencyResolver->setOuterMostParentsRequireReferences(true); $this->dependencyResolver->setWorkspace($this->getWorkspace()); @@ -87,7 +87,7 @@ class CollectionService implements \TYPO3\CMS\Core\SingletonInterface protected function getDependencyCallback($method, array $targetArguments = []) { return GeneralUtility::makeInstance( - \TYPO3\CMS\Version\Dependency\EventCallback::class, + \TYPO3\CMS\Workspaces\Dependency\EventCallback::class, $this->getElementEntityProcessor(), $method, $targetArguments diff --git a/typo3/sysext/workspaces/Migrations/Code/ClassAliasMap.php b/typo3/sysext/workspaces/Migrations/Code/ClassAliasMap.php index 38a19d0d9229d50a5047c82b2db85760b24d71c7..0394c52e8959327b9729a4b1b614b643499f5b35 100644 --- a/typo3/sysext/workspaces/Migrations/Code/ClassAliasMap.php +++ b/typo3/sysext/workspaces/Migrations/Code/ClassAliasMap.php @@ -1,4 +1,15 @@ <?php return [ 'TYPO3\\CMS\\Lowlevel\\Command\\WorkspaceVersionRecordsCommand' => \TYPO3\CMS\Workspaces\Command\WorkspaceVersionRecordsCommand::class, + 'TYPO3\\CMS\\Version\\DataHandler\\CommandMap' => \TYPO3\CMS\Workspaces\DataHandler\CommandMap::class, + 'TYPO3\\CMS\\Version\\Dependency\\DependencyEntityFactory' => \TYPO3\CMS\Workspaces\Dependency\DependencyEntityFactory::class, + 'TYPO3\\CMS\\Version\\Dependency\\DependencyResolver' => \TYPO3\CMS\Workspaces\Dependency\DependencyResolver::class, + 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity' => \TYPO3\CMS\Workspaces\Dependency\ElementEntity::class, + 'TYPO3\\CMS\\Version\\Dependency\\ElementEntityProcessor' => \TYPO3\CMS\Workspaces\Dependency\ElementEntityProcessor::class, + 'TYPO3\\CMS\\Version\\Dependency\\EventCallback' => \TYPO3\CMS\Workspaces\Dependency\EventCallback::class, + 'TYPO3\\CMS\\Version\\Dependency\\ReferenceEntity' => \TYPO3\CMS\Workspaces\Dependency\ReferenceEntity::class, + 'TYPO3\\CMS\\Version\\Hook\\DataHandlerHook' => \TYPO3\CMS\Workspaces\Hook\DataHandlerHook::class, + 'TYPO3\\CMS\\Version\\Hook\\PreviewHook' => \TYPO3\CMS\Workspaces\Hook\PreviewHook::class, + 'TYPO3\\CMS\\Version\\Task\\AutoPublishTask' => \TYPO3\CMS\Workspaces\Task\AutoPublishTask::class, + 'TYPO3\\CMS\\Version\\Utility\\WorkspacesUtility' => \TYPO3\CMS\Workspaces\Service\WorkspaceService::class, ]; diff --git a/typo3/sysext/version/Resources/Private/Language/locallang_emails.xlf b/typo3/sysext/workspaces/Resources/Private/Language/locallang_emails.xlf similarity index 98% rename from typo3/sysext/version/Resources/Private/Language/locallang_emails.xlf rename to typo3/sysext/workspaces/Resources/Private/Language/locallang_emails.xlf index 9f43bf681326d1087f73d7932796ba54b778fc50..91fc47b4385ba17776426970a284e9f184be0292 100644 --- a/typo3/sysext/version/Resources/Private/Language/locallang_emails.xlf +++ b/typo3/sysext/workspaces/Resources/Private/Language/locallang_emails.xlf @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <xliff version="1.0" xmlns:t3="http://typo3.org/schemas/xliff"> - <file t3:id="1415815010" source-language="en" datatype="plaintext" original="messages" date="2011-10-17T20:22:37Z" product-name="version"> + <file t3:id="1415815010" source-language="en" datatype="plaintext" original="messages" date="2011-10-17T20:22:37Z" product-name="workspaces"> <header/> <body> <trans-unit id="subject"> diff --git a/typo3/sysext/workspaces/Tests/Functional/ActionHandler/ActionHandlerTest.php b/typo3/sysext/workspaces/Tests/Functional/ActionHandler/ActionHandlerTest.php index 40ed63b024e000a46a9324db7676def9c2d6cfbf..28a603a1f188c7af45493b21f0723fe8473fdef9 100644 --- a/typo3/sysext/workspaces/Tests/Functional/ActionHandler/ActionHandlerTest.php +++ b/typo3/sysext/workspaces/Tests/Functional/ActionHandler/ActionHandlerTest.php @@ -24,7 +24,7 @@ class ActionHandlerTest extends \TYPO3\TestingFramework\Core\Functional\Function /** * @var array */ - protected $coreExtensionsToLoad = ['version', 'workspaces']; + protected $coreExtensionsToLoad = ['workspaces']; /** * Set up diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/FAL/AbstractActionTestCase.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/FAL/AbstractActionTestCase.php index 5966409d92fe604f3e602af7bfc2016b1ddc0464..65704a6f1fa36f2ed888597e329d16fd4aa5f72f 100644 --- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/FAL/AbstractActionTestCase.php +++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/FAL/AbstractActionTestCase.php @@ -31,7 +31,6 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D */ protected $coreExtensionsToLoad = [ 'fluid', - 'version', 'workspaces', ]; diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php index 901ee06bfd327645122d432d373bb6e91ac890a8..f36c84d2793a8322d3c22f46afb0851b5d32cd60 100644 --- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php +++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php @@ -31,7 +31,6 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D */ protected $coreExtensionsToLoad = [ 'fluid', - 'version', 'workspaces', ]; diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php index f9aa0efbf88c2a6e0015f058962304b24bbec2c7..68220e1c5d76a4d977420725e5bf1e0efb954c9d 100644 --- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php +++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php @@ -31,7 +31,6 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D */ protected $coreExtensionsToLoad = [ 'fluid', - 'version', 'workspaces', ]; diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php index c73f275ecb619fb208d0852d6aa834418d5b7192..ec59a4be36d557e26e67fda74e40156bed6c847c 100644 --- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php +++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php @@ -31,7 +31,6 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D */ protected $coreExtensionsToLoad = [ 'fluid', - 'version', 'workspaces', ]; diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/AbstractActionTestCase.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/AbstractActionTestCase.php index 03bc55294d0c822db00ecb44bd3823d0ac48415a..db227695cb1c015b6ab0b7b29560a57479bdcd5d 100644 --- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/AbstractActionTestCase.php +++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/AbstractActionTestCase.php @@ -32,7 +32,6 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D */ protected $coreExtensionsToLoad = [ 'fluid', - 'version', 'workspaces', ]; diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php index 26863222221f8105ac6440dbf38f756545d09a81..dd26a064719cdb7fa61319be79ed34f7ed711998 100644 --- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php +++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php @@ -33,7 +33,6 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D */ protected $coreExtensionsToLoad = [ 'fluid', - 'version', 'workspaces', ]; diff --git a/typo3/sysext/workspaces/Tests/Functional/DataHandling/Select/AbstractActionTestCase.php b/typo3/sysext/workspaces/Tests/Functional/DataHandling/Select/AbstractActionTestCase.php index c3bb4dcce2f691d1f655ab361e8168295ff6bbbb..86f0005076fcacf5a12f7061e0b503e6738d75d6 100644 --- a/typo3/sysext/workspaces/Tests/Functional/DataHandling/Select/AbstractActionTestCase.php +++ b/typo3/sysext/workspaces/Tests/Functional/DataHandling/Select/AbstractActionTestCase.php @@ -31,7 +31,6 @@ abstract class AbstractActionTestCase extends \TYPO3\CMS\Core\Tests\Functional\D */ protected $coreExtensionsToLoad = [ 'fluid', - 'version', 'workspaces', ]; diff --git a/typo3/sysext/workspaces/Tests/Functional/Service/WorkspaceServiceTest.php b/typo3/sysext/workspaces/Tests/Functional/Service/WorkspaceServiceTest.php index 88a6884cdae7451ad6934e70656780b8c8a86e99..c8ecf179ec0d2b17589ab06aa0818a275c7c9c89 100644 --- a/typo3/sysext/workspaces/Tests/Functional/Service/WorkspaceServiceTest.php +++ b/typo3/sysext/workspaces/Tests/Functional/Service/WorkspaceServiceTest.php @@ -26,7 +26,7 @@ class WorkspaceServiceTest extends FunctionalTestCase /** * @var array */ - protected $coreExtensionsToLoad = ['version', 'workspaces']; + protected $coreExtensionsToLoad = ['workspaces']; /** * Set up diff --git a/typo3/sysext/workspaces/composer.json b/typo3/sysext/workspaces/composer.json index 1f5d2f4a80ab5d8b40e723e0c1530ac124f783de..4eb1abfa81e0a9851e243417f4e981b32d471e68 100644 --- a/typo3/sysext/workspaces/composer.json +++ b/typo3/sysext/workspaces/composer.json @@ -11,8 +11,7 @@ }], "require": { - "typo3/cms-core": ">=9.0.0 <=9.0.99", - "typo3/cms-version": ">=9.0.0 <=9.0.99" + "typo3/cms-core": ">=9.0.0 <=9.0.99" }, "conflict": { "typo3/cms": "*" diff --git a/typo3/sysext/workspaces/ext_emconf.php b/typo3/sysext/workspaces/ext_emconf.php index b8b00d119fa7f981549d5039cc40edec136d22d9..d5d684c5a2488ae937317e74c4d9c282f6a7d97d 100644 --- a/typo3/sysext/workspaces/ext_emconf.php +++ b/typo3/sysext/workspaces/ext_emconf.php @@ -1,7 +1,7 @@ <?php $EM_CONF[$_EXTKEY] = [ 'title' => 'Workspaces Management', - 'description' => 'Adds workspaces functionality with custom stages to TYPO3.', + 'description' => 'Adds versioning of records and workspaces functionality with custom stages to TYPO3.', 'category' => 'be', 'author' => 'TYPO3 Core Team', 'author_email' => 'typo3cms@typo3.org', @@ -13,8 +13,7 @@ $EM_CONF[$_EXTKEY] = [ 'version' => '9.0.0', 'constraints' => [ 'depends' => [ - 'typo3' => '9.0.0-9.0.99', - 'version' => '9.0.0-9.0.99', + 'typo3' => '9.0.0-9.0.99' ], 'conflicts' => [], 'suggests' => [], diff --git a/typo3/sysext/workspaces/ext_localconf.php b/typo3/sysext/workspaces/ext_localconf.php index 5797317942f261ebadbf1b65ef6dd76493d9b1c1..a8cf15a86fc93d502194200e2647642a6f075762 100644 --- a/typo3/sysext/workspaces/ext_localconf.php +++ b/typo3/sysext/workspaces/ext_localconf.php @@ -1,6 +1,13 @@ <?php defined('TYPO3_MODE') or die(); +// add default notification options to every page +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPageTSConfig(' +tx_version.workspaces.stageNotificationEmail.subject = LLL:EXT:workspaces/Resources/Private/Language/locallang_emails.xlf:subject +tx_version.workspaces.stageNotificationEmail.message = LLL:EXT:workspaces/Resources/Private/Language/locallang_emails.xlf:message +# tx_version.workspaces.stageNotificationEmail.additionalHeaders = +'); + // Register the autopublishing task $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Workspaces\Task\AutoPublishTask::class] = [ 'extension' => 'workspaces', @@ -15,7 +22,9 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Works 'description' => 'LLL:EXT:workspaces/Resources/Private/Language/locallang_mod.xlf:cleanupPreviewLinkTask.description' ]; +// register the hook to actually do the work within DataHandler $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processCmdmapClass']['workspaces'] = \TYPO3\CMS\Workspaces\Hook\DataHandlerHook::class; +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['moveRecordClass']['version'] = \TYPO3\CMS\Workspaces\Hook\DataHandlerHook::class; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass']['workspaces'] = \TYPO3\CMS\Workspaces\Hook\BackendUtilityHook::class; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_previewInfo']['workspaces'] = \TYPO3\CMS\Workspaces\Hook\TypoScriptFrontendControllerHook::class . '->renderPreviewInfo'; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/alt_doc.php']['makeEditForm_accessCheck']['workspaces'] = \TYPO3\CMS\Workspaces\Hook\BackendUtilityHook::class . '->makeEditForm_accessCheck';