[!!!][TASK] Migrate EXT:version into EXT:workspaces 14/54514/8
authorBenni Mack <benni@typo3.org>
Tue, 31 Oct 2017 20:24:43 +0000 (21:24 +0100)
committerSusanne Moog <susanne.moog@typo3.org>
Thu, 2 Nov 2017 10:49:16 +0000 (11:49 +0100)
The concept of versioning records based on how it is
integrated into TYPO3 only makes sense in conjunction
with workspaces / stages. All other methods are not
supported. Thus, the logic of versioning of records
is moved into EXT:workspaces, making EXT:version
obsolete.

Checks against EXT:version is now handled against
"workspaces" throughout the core, with a fallback
within ExtensionManagementUtility::isLoaded().

Resolves: #82896
Releases: master
Change-Id: I8887b868892c8aa6c36db7d2841f6edf5476d6f7
Reviewed-on: https://review.typo3.org/54514
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
51 files changed:
composer.json
composer.lock
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Classes/View/PageLayoutView.php
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Classes/DataHandling/PlainDataResolver.php
typo3/sysext/core/Classes/Utility/ExtensionManagementUtility.php
typo3/sysext/core/Documentation/Changelog/master/Breaking-82896-SystemExtensionVersionMigratedIntoWorkspaces.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Functional/Fixtures/Extensions/irre_tutorial/ext_emconf.php
typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_datahandler/ext_emconf.php
typo3/sysext/recordlist/Classes/RecordList/AbstractDatabaseRecordList.php
typo3/sysext/recordlist/Classes/RecordList/DatabaseRecordList.php
typo3/sysext/version/Classes/DataHandler/CommandMap.php [deleted file]
typo3/sysext/version/Classes/Dependency/DependencyEntityFactory.php [deleted file]
typo3/sysext/version/Classes/Dependency/DependencyResolver.php [deleted file]
typo3/sysext/version/Classes/Dependency/ElementEntity.php [deleted file]
typo3/sysext/version/Classes/Dependency/ElementEntityProcessor.php [deleted file]
typo3/sysext/version/Classes/Dependency/EventCallback.php [deleted file]
typo3/sysext/version/Classes/Dependency/ReferenceEntity.php [deleted file]
typo3/sysext/version/Classes/Hook/DataHandlerHook.php [deleted file]
typo3/sysext/version/LICENSE.txt [deleted file]
typo3/sysext/version/Migrations/Code/ClassAliasMap.php [deleted file]
typo3/sysext/version/Resources/Private/Language/locallang_emails.xlf [deleted file]
typo3/sysext/version/Resources/Public/Icons/Extension.png [deleted file]
typo3/sysext/version/Resources/Public/Icons/module-version.svg [deleted file]
typo3/sysext/version/composer.json [deleted file]
typo3/sysext/version/ext_emconf.php [deleted file]
typo3/sysext/version/ext_localconf.php [deleted file]
typo3/sysext/workspaces/Classes/DataHandler/CommandMap.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Dependency/DependencyEntityFactory.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Dependency/DependencyResolver.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Dependency/ElementEntity.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Dependency/ElementEntityProcessor.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Dependency/EventCallback.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Dependency/ReferenceEntity.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Hook/DataHandlerHook.php
typo3/sysext/workspaces/Classes/Service/Dependency/CollectionService.php
typo3/sysext/workspaces/Migrations/Code/ClassAliasMap.php
typo3/sysext/workspaces/Resources/Private/Language/locallang_emails.xlf [new file with mode: 0644]
typo3/sysext/workspaces/Tests/Functional/ActionHandler/ActionHandlerTest.php
typo3/sysext/workspaces/Tests/Functional/DataHandling/FAL/AbstractActionTestCase.php
typo3/sysext/workspaces/Tests/Functional/DataHandling/Group/AbstractActionTestCase.php
typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/CSV/AbstractActionTestCase.php
typo3/sysext/workspaces/Tests/Functional/DataHandling/IRRE/ForeignField/AbstractActionTestCase.php
typo3/sysext/workspaces/Tests/Functional/DataHandling/ManyToMany/AbstractActionTestCase.php
typo3/sysext/workspaces/Tests/Functional/DataHandling/Regular/AbstractActionTestCase.php
typo3/sysext/workspaces/Tests/Functional/DataHandling/Select/AbstractActionTestCase.php
typo3/sysext/workspaces/Tests/Functional/Service/WorkspaceServiceTest.php
typo3/sysext/workspaces/composer.json
typo3/sysext/workspaces/ext_emconf.php
typo3/sysext/workspaces/ext_localconf.php

index d987be0..62a4f1b 100644 (file)
@@ -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"
                        ]
                },
                        "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/"
                },
index d90f8d8..772b402 100644 (file)
@@ -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",
index 02faf4a..1c11d10 100644 (file)
@@ -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;
index 8a54801..7df137c 100644 (file)
@@ -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';
index 3a383d0..f3a3e1f 100644 (file)
@@ -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;
     }
 
index ba335d3..cbe90cf 100644 (file)
@@ -372,7 +372,7 @@ class PlainDataResolver
      */
     protected function isWorkspaceEnabled()
     {
-        if (ExtensionManagementUtility::isLoaded('version')) {
+        if (ExtensionManagementUtility::isLoaded('workspaces')) {
             return BackendUtility::isTableWorkspaceEnabled($this->tableName);
         }
         return false;
index debe42a..b4989b4 100644 (file)
@@ -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 (file)
index 0000000..e864148
--- /dev/null
@@ -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
index a3b5e1c..d0daa7c 100644 (file)
@@ -15,7 +15,6 @@ $EM_CONF[$_EXTKEY] = [
         'depends' => [
             'typo3' => '4.5.0-0.0.0',
             'workspaces' => '0.0.0-',
-            'version' => '0.0.0-',
         ],
         'conflicts' => [
         ],
index e5ce8ff..abfcffc 100644 (file)
@@ -15,7 +15,6 @@ $EM_CONF[$_EXTKEY] = [
         'depends' => [
             'typo3' => '6.0.0-0.0.0',
             'workspaces' => '0.0.0-',
-            'version' => '0.0.0-',
         ],
         'conflicts' => [
         ],
index 381b521..e3c79fe 100644 (file)
@@ -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';
index 686403a..3d5f0c7 100644 (file)
@@ -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/DataHandler/CommandMap.php b/typo3/sysext/version/Classes/DataHandler/CommandMap.php
deleted file mode 100644 (file)
index 3f2315f..0000000
+++ /dev/null
@@ -1,808 +0,0 @@
-<?php
-namespace TYPO3\CMS\Version\DataHandler;
-
-/*
- * 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 TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Version\Dependency\ElementEntity;
-
-/**
- * Handles the \TYPO3\CMS\Core\DataHandling\DataHandler command map and is
- * only used in combination with \TYPO3\CMS\Core\DataHandling\DataHandler
- */
-class CommandMap
-{
-    const SCOPE_WorkspacesSwap = 'SCOPE_WorkspacesSwap';
-    const SCOPE_WorkspacesSetStage = 'SCOPE_WorkspacesSetStage';
-    const SCOPE_WorkspacesClear = 'SCOPE_WorkspacesClear';
-    const KEY_GetElementPropertiesCallback = 'KEY_GetElementPropertiesCallback';
-    const KEY_GetCommonPropertiesCallback = 'KEY_GetCommonPropertiesCallback';
-    const KEY_ElementConstructCallback = 'KEY_EventConstructCallback';
-    const KEY_ElementCreateChildReferenceCallback = 'KEY_ElementCreateChildReferenceCallback';
-    const KEY_ElementCreateParentReferenceCallback = 'KEY_ElementCreateParentReferenceCallback';
-    const KEY_UpdateGetIdCallback = 'KEY_UpdateGetIdCallback';
-    const KEY_TransformDependentElementsToUseLiveId = 'KEY_TransformDependentElementsToUseLiveId';
-
-    /**
-     * @var \TYPO3\CMS\Version\Hook\DataHandlerHook
-     */
-    protected $parent;
-
-    /**
-     * @var \TYPO3\CMS\Core\DataHandling\DataHandler
-     */
-    protected $tceMain;
-
-    /**
-     * @var array
-     */
-    protected $commandMap = [];
-
-    /**
-     * @var int
-     */
-    protected $workspace;
-
-    /**
-     * @var string
-     */
-    protected $workspacesSwapMode;
-
-    /**
-     * @var string
-     */
-    protected $workspacesChangeStageMode;
-
-    /**
-     * @var array
-     */
-    protected $scopes;
-
-    /**
-     * @var \TYPO3\CMS\Version\Dependency\ElementEntityProcessor
-     */
-    protected $elementEntityProcessor;
-
-    /**
-     * Creates this object.
-     *
-     * @param \TYPO3\CMS\Version\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)
-    {
-        $this->setParent($parent);
-        $this->setTceMain($tceMain);
-        $this->set($commandMap);
-        $this->setWorkspace($workspace);
-        $this->setWorkspacesSwapMode($this->getTceMain()->BE_USER->getTSConfigVal('options.workspaces.swapMode'));
-        $this->setWorkspacesChangeStageMode($this->getTceMain()->BE_USER->getTSConfigVal('options.workspaces.changeStageMode'));
-        $this->constructScopes();
-    }
-
-    /**
-     * Gets the command map.
-     *
-     * @return array
-     */
-    public function get()
-    {
-        return $this->commandMap;
-    }
-
-    /**
-     * Sets the command map.
-     *
-     * @param array $commandMap
-     * @return \TYPO3\CMS\Version\DataHandler\CommandMap
-     */
-    public function set(array $commandMap)
-    {
-        $this->commandMap = $commandMap;
-        return $this;
-    }
-
-    /**
-     * Gets the parent object.
-     *
-     * @return \TYPO3\CMS\Version\Hook\DataHandlerHook
-     */
-    public function getParent()
-    {
-        return $this->parent;
-    }
-
-    /**
-     * Sets the parent object.
-     *
-     * @param \TYPO3\CMS\Version\Hook\DataHandlerHook $parent
-     * @return \TYPO3\CMS\Version\DataHandler\CommandMap
-     */
-    public function setParent(\TYPO3\CMS\Version\Hook\DataHandlerHook $parent)
-    {
-        $this->parent = $parent;
-        return $this;
-    }
-
-    /**
-     * Gets the parent object.
-     *
-     * @return \TYPO3\CMS\Core\DataHandling\DataHandler
-     */
-    public function getTceMain()
-    {
-        return $this->tceMain;
-    }
-
-    /**
-     * Sets the parent object.
-     *
-     * @param \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain
-     * @return \TYPO3\CMS\Version\DataHandler\CommandMap
-     */
-    public function setTceMain(\TYPO3\CMS\Core\DataHandling\DataHandler $tceMain)
-    {
-        $this->tceMain = $tceMain;
-        return $this;
-    }
-
-    /**
-     * Sets the current workspace.
-     *
-     * @param int $workspace
-     */
-    public function setWorkspace($workspace)
-    {
-        $this->workspace = (int)$workspace;
-    }
-
-    /**
-     * Gets the current workspace.
-     *
-     * @return int
-     */
-    public function getWorkspace()
-    {
-        return $this->workspace;
-    }
-
-    /**
-     * Sets the workspaces swap mode
-     * (see options.workspaces.swapMode).
-     *
-     * @param string $workspacesSwapMode
-     * @return \TYPO3\CMS\Version\DataHandler\CommandMap
-     */
-    public function setWorkspacesSwapMode($workspacesSwapMode)
-    {
-        $this->workspacesSwapMode = (string)$workspacesSwapMode;
-        return $this;
-    }
-
-    /**
-     * Sets the workspaces change stage mode
-     * see options.workspaces.changeStageMode)
-     *
-     * @param string $workspacesChangeStageMode
-     * @return \TYPO3\CMS\Version\DataHandler\CommandMap
-     */
-    public function setWorkspacesChangeStageMode($workspacesChangeStageMode)
-    {
-        $this->workspacesChangeStageMode = (string)$workspacesChangeStageMode;
-        return $this;
-    }
-
-    /**
-     * Gets the element entity processor.
-     *
-     * @return \TYPO3\CMS\Version\Dependency\ElementEntityProcessor
-     */
-    protected function getElementEntityProcessor()
-    {
-        if (!isset($this->elementEntityProcessor)) {
-            $this->elementEntityProcessor = GeneralUtility::makeInstance(
-                \TYPO3\CMS\Version\Dependency\ElementEntityProcessor::class
-            );
-            $this->elementEntityProcessor->setWorkspace($this->getWorkspace());
-        }
-        return $this->elementEntityProcessor;
-    }
-
-    /**
-     * Processes the command map.
-     *
-     * @return \TYPO3\CMS\Version\DataHandler\CommandMap
-     */
-    public function process()
-    {
-        $this->resolveWorkspacesSwapDependencies();
-        $this->resolveWorkspacesSetStageDependencies();
-        $this->resolveWorkspacesClearDependencies();
-        return $this;
-    }
-
-    /**
-     * Invokes all items for swapping/publishing with a callback method.
-     *
-     * @param string $callbackMethod
-     * @param array $arguments Optional leading arguments for the callback method
-     */
-    protected function invokeWorkspacesSwapItems($callbackMethod, array $arguments = [])
-    {
-        // Traverses the cmd[] array and fetches the accordant actions:
-        foreach ($this->commandMap as $table => $liveIdCollection) {
-            foreach ($liveIdCollection as $liveId => $commandCollection) {
-                foreach ($commandCollection as $command => $properties) {
-                    if ($command === 'version' && isset($properties['action']) && $properties['action'] === 'swap') {
-                        if (isset($properties['swapWith']) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($properties['swapWith'])) {
-                            call_user_func_array([$this, $callbackMethod], array_merge($arguments, [$table, $liveId, $properties]));
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Resolves workspaces related dependencies for swapping/publishing of the command map.
-     * Workspaces records that have children or (relative) parents which are versionized
-     * but not published with this request, are removed from the command map. Otherwise
-     * this would produce hanging record sets and lost references.
-     */
-    protected function resolveWorkspacesSwapDependencies()
-    {
-        $scope = self::SCOPE_WorkspacesSwap;
-        $dependency = $this->getDependencyUtility($scope);
-        if ($this->workspacesSwapMode === 'any' || $this->workspacesSwapMode === 'pages') {
-            $this->invokeWorkspacesSwapItems('applyWorkspacesSwapBehaviour');
-        }
-        $this->invokeWorkspacesSwapItems('addWorkspacesSwapElements', [$dependency]);
-        $this->applyWorkspacesDependencies($dependency, $scope);
-    }
-
-    /**
-     * Applies workspaces behaviour for swapping/publishing and takes care of the swapMode.
-     *
-     * @param string $table
-     * @param int $liveId
-     * @param array $properties
-     */
-    protected function applyWorkspacesSwapBehaviour($table, $liveId, array $properties)
-    {
-        $extendedCommandMap = [];
-        $elementList = [];
-        // Fetch accordant elements if the swapMode is 'any' or 'pages':
-        if ($this->workspacesSwapMode === 'any' || $this->workspacesSwapMode === 'pages' && $table === 'pages') {
-            $elementList = $this->getParent()->findPageElementsForVersionSwap($table, $liveId, $properties['swapWith']);
-        }
-        foreach ($elementList as $elementTable => $elementIdArray) {
-            foreach ($elementIdArray as $elementIds) {
-                $extendedCommandMap[$elementTable][$elementIds[0]]['version'] = array_merge($properties, ['swapWith' => $elementIds[1]]);
-            }
-        }
-        if (!empty($elementList)) {
-            $this->remove($table, $liveId, 'version');
-            $this->mergeToBottom($extendedCommandMap);
-        }
-    }
-
-    /**
-     * Adds workspaces elements for swapping/publishing.
-     *
-     * @param \TYPO3\CMS\Version\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)
-    {
-        $elementList = [];
-        // Fetch accordant elements if the swapMode is 'any' or 'pages':
-        if ($this->workspacesSwapMode === 'any' || $this->workspacesSwapMode === 'pages' && $table === 'pages') {
-            $elementList = $this->getParent()->findPageElementsForVersionSwap($table, $liveId, $properties['swapWith']);
-        }
-        foreach ($elementList as $elementTable => $elementIdArray) {
-            foreach ($elementIdArray as $elementIds) {
-                $dependency->addElement($elementTable, $elementIds[1], ['liveId' => $elementIds[0], 'properties' => array_merge($properties, ['swapWith' => $elementIds[1]])]);
-            }
-        }
-        if (empty($elementList)) {
-            $dependency->addElement($table, $properties['swapWith'], ['liveId' => $liveId, 'properties' => $properties]);
-        }
-    }
-
-    /**
-     * Invokes all items for staging with a callback method.
-     *
-     * @param string $callbackMethod
-     * @param array $arguments Optional leading arguments for the callback method
-     */
-    protected function invokeWorkspacesSetStageItems($callbackMethod, array $arguments = [])
-    {
-        // Traverses the cmd[] array and fetches the accordant actions:
-        foreach ($this->commandMap as $table => $versionIdCollection) {
-            foreach ($versionIdCollection as $versionIdList => $commandCollection) {
-                foreach ($commandCollection as $command => $properties) {
-                    if ($command === 'version' && isset($properties['action']) && $properties['action'] === 'setStage') {
-                        if (isset($properties['stageId']) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($properties['stageId'])) {
-                            call_user_func_array([$this, $callbackMethod], array_merge($arguments, [$table, $versionIdList, $properties]));
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Resolves workspaces related dependencies for staging of the command map.
-     * Workspaces records that have children or (relative) parents which are versionized
-     * but not staged with this request, are removed from the command map.
-     */
-    protected function resolveWorkspacesSetStageDependencies()
-    {
-        $scope = self::SCOPE_WorkspacesSetStage;
-        $dependency = $this->getDependencyUtility($scope);
-        if ($this->workspacesChangeStageMode === 'any' || $this->workspacesChangeStageMode === 'pages') {
-            $this->invokeWorkspacesSetStageItems('applyWorkspacesSetStageBehaviour');
-        }
-        $this->invokeWorkspacesSetStageItems('explodeSetStage');
-        $this->invokeWorkspacesSetStageItems('addWorkspacesSetStageElements', [$dependency]);
-        $this->applyWorkspacesDependencies($dependency, $scope);
-    }
-
-    /**
-     * Applies workspaces behaviour for staging and takes care of the changeStageMode.
-     *
-     * @param string $table
-     * @param string $versionIdList
-     * @param array $properties
-     */
-    protected function applyWorkspacesSetStageBehaviour($table, $versionIdList, array $properties)
-    {
-        $extendedCommandMap = [];
-        $versionIds = GeneralUtility::trimExplode(',', $versionIdList, true);
-        $elementList = [$table => $versionIds];
-        if ($this->workspacesChangeStageMode === 'any' || $this->workspacesChangeStageMode === 'pages') {
-            if (count($versionIds) === 1) {
-                $workspaceRecord = BackendUtility::getRecord($table, $versionIds[0], 't3ver_wsid');
-                $workspaceId = $workspaceRecord['t3ver_wsid'];
-            } else {
-                $workspaceId = $this->getWorkspace();
-            }
-            if ($table === 'pages') {
-                // Find all elements from the same ws to change stage
-                $livePageIds = $versionIds;
-                $this->getParent()->findRealPageIds($livePageIds);
-                $this->getParent()->findPageElementsForVersionStageChange($livePageIds, $workspaceId, $elementList);
-            } elseif ($this->workspacesChangeStageMode === 'any') {
-                // Find page to change stage:
-                $pageIdList = [];
-                $this->getParent()->findPageIdsForVersionStateChange($table, $versionIds, $workspaceId, $pageIdList, $elementList);
-                // Find other elements from the same ws to change stage:
-                $this->getParent()->findPageElementsForVersionStageChange($pageIdList, $workspaceId, $elementList);
-            }
-        }
-        foreach ($elementList as $elementTable => $elementIds) {
-            foreach ($elementIds as $elementId) {
-                $extendedCommandMap[$elementTable][$elementId]['version'] = $properties;
-            }
-        }
-        $this->remove($table, $versionIds, 'version');
-        $this->mergeToBottom($extendedCommandMap);
-    }
-
-    /**
-     * Adds workspaces elements for staging.
-     *
-     * @param \TYPO3\CMS\Version\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)
-    {
-        $dependency->addElement($table, $versionId, ['versionId' => $versionId, 'properties' => $properties]);
-    }
-
-    /**
-     * Resolves workspaces related dependencies for clearing/flushing of the command map.
-     * Workspaces records that have children or (relative) parents which are versionized
-     * but not cleared/flushed with this request, are removed from the command map.
-     */
-    protected function resolveWorkspacesClearDependencies()
-    {
-        $scope = self::SCOPE_WorkspacesClear;
-        $dependency = $this->getDependencyUtility($scope);
-        // Traverses the cmd[] array and fetches the accordant actions:
-        foreach ($this->commandMap as $table => $versionIdCollection) {
-            foreach ($versionIdCollection as $versionId => $commandCollection) {
-                foreach ($commandCollection as $command => $properties) {
-                    if ($command === 'version' && isset($properties['action']) && ($properties['action'] === 'clearWSID' || $properties['action'] === 'flush')) {
-                        $dependency->addElement($table, $versionId, ['versionId' => $versionId, 'properties' => $properties]);
-                    }
-                }
-            }
-        }
-        $this->applyWorkspacesDependencies($dependency, $scope);
-    }
-
-    /**
-     * Explodes id-lists in the command map for staging actions.
-     *
-     * @throws \RuntimeException
-     * @param string $table
-     * @param string $versionIdList
-     * @param array $properties
-     */
-    protected function explodeSetStage($table, $versionIdList, array $properties)
-    {
-        $extractedCommandMap = [];
-        $versionIds = GeneralUtility::trimExplode(',', $versionIdList, true);
-        if (count($versionIds) > 1) {
-            foreach ($versionIds as $versionId) {
-                if (isset($this->commandMap[$table][$versionId]['version'])) {
-                    throw new \RuntimeException('Command map for [' . $table . '][' . $versionId . '][version] was already set.', 1289391048);
-                }
-                $extractedCommandMap[$table][$versionId]['version'] = $properties;
-            }
-            $this->remove($table, $versionIdList, 'version');
-            $this->mergeToBottom($extractedCommandMap);
-        }
-    }
-
-    /**
-     * Applies the workspaces dependencies and removes incomplete structures or automatically
-     * completes them
-     *
-     * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency
-     * @param string $scope
-     */
-    protected function applyWorkspacesDependencies(\TYPO3\CMS\Version\Dependency\DependencyResolver $dependency, $scope)
-    {
-        $transformDependentElementsToUseLiveId = $this->getScopeData($scope, self::KEY_TransformDependentElementsToUseLiveId);
-        $elementsToBeVersioned = $dependency->getElements();
-        // Use the uid of the live record instead of the workspace record:
-        if ($transformDependentElementsToUseLiveId) {
-            $elementsToBeVersioned = $this->getElementEntityProcessor()->transformDependentElementsToUseLiveId($elementsToBeVersioned);
-        }
-        $outerMostParents = $dependency->getOuterMostParents();
-        /** @var $outerMostParent ElementEntity */
-        foreach ($outerMostParents as $outerMostParent) {
-            $dependentElements = $dependency->getNestedElements($outerMostParent);
-            if ($transformDependentElementsToUseLiveId) {
-                $dependentElements = $this->getElementEntityProcessor()->transformDependentElementsToUseLiveId($dependentElements);
-            }
-            // Gets the difference (intersection) between elements that were submitted by the user
-            // and the evaluation of all dependent records that should be used for this action instead:
-            $intersectingElements = array_intersect_key($dependentElements, $elementsToBeVersioned);
-            if (!empty($intersectingElements)) {
-                $this->update(current($intersectingElements), $dependentElements, $scope);
-            }
-        }
-    }
-
-    /**
-     * Updates the command map accordant to valid structures and takes care of the correct order.
-     *
-     * @param ElementEntity $intersectingElement
-     * @param array $elements
-     * @param string $scope
-     */
-    protected function update(ElementEntity $intersectingElement, array $elements, $scope)
-    {
-        $orderedCommandMap = [];
-        $commonProperties = [];
-        if ($this->getScopeData($scope, self::KEY_GetCommonPropertiesCallback)) {
-            $commonProperties = $this->processCallback($this->getScopeData($scope, self::KEY_GetCommonPropertiesCallback), [$intersectingElement]);
-        }
-        /** @var $element ElementEntity */
-        foreach ($elements as $element) {
-            $table = $element->getTable();
-            $id = $this->processCallback($this->getScopeData($scope, self::KEY_UpdateGetIdCallback), [$element]);
-            $this->remove($table, $id, 'version');
-            if ($element->isInvalid()) {
-                continue;
-            }
-            $orderedCommandMap[$table][$id]['version'] = $commonProperties;
-            if ($this->getScopeData($scope, self::KEY_GetElementPropertiesCallback)) {
-                $orderedCommandMap[$table][$id]['version'] = array_merge($commonProperties, $this->processCallback($this->getScopeData($scope, self::KEY_GetElementPropertiesCallback), [$element]));
-            }
-        }
-        // Ensure that ordered command map is on top of the command map:
-        $this->mergeToTop($orderedCommandMap);
-    }
-
-    /**
-     * Merges command map elements to the top of the current command map..
-     *
-     * @param array $commandMap
-     */
-    protected function mergeToTop(array $commandMap)
-    {
-        \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($commandMap, $this->commandMap);
-        $this->commandMap = $commandMap;
-    }
-
-    /**
-     * Merges command map elements to the bottom of the current command map.
-     *
-     * @param array $commandMap
-     */
-    protected function mergeToBottom(array $commandMap)
-    {
-        \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($this->commandMap, $commandMap);
-    }
-
-    /**
-     * Removes an element from the command map.
-     *
-     * @param string $table
-     * @param string $id
-     * @param string $command (optional)
-     */
-    protected function remove($table, $id, $command = null)
-    {
-        if (is_string($command)) {
-            unset($this->commandMap[$table][$id][$command]);
-        } else {
-            unset($this->commandMap[$table][$id]);
-        }
-    }
-
-    /**
-     * Callback to get the liveId of an dependent element.
-     *
-     * @param ElementEntity $element
-     * @return int
-     */
-    protected function getElementLiveIdCallback(ElementEntity $element)
-    {
-        return $element->getDataValue('liveId');
-    }
-
-    /**
-     * Callback to get the real id of an dependent element.
-     *
-     * @param ElementEntity $element
-     * @return int
-     */
-    protected function getElementIdCallback(ElementEntity $element)
-    {
-        return $element->getId();
-    }
-
-    /**
-     * Callback to get the specific properties of a dependent element for swapping/publishing.
-     *
-     * @param ElementEntity $element
-     * @return array
-     */
-    protected function getElementSwapPropertiesCallback(ElementEntity $element)
-    {
-        return [
-            'swapWith' => $element->getId()
-        ];
-    }
-
-    /**
-     * Callback to get common properties of dependent elements for clearing.
-     *
-     * @param ElementEntity $element
-     * @return array
-     */
-    protected function getCommonClearPropertiesCallback(ElementEntity $element)
-    {
-        $commonSwapProperties = [];
-        $elementProperties = $element->getDataValue('properties');
-        if (isset($elementProperties['action'])) {
-            $commonSwapProperties['action'] = $elementProperties['action'];
-        }
-        return $commonSwapProperties;
-    }
-
-    /**
-     * Callback to get common properties of dependent elements for swapping/publishing.
-     *
-     * @param ElementEntity $element
-     * @return array
-     */
-    protected function getCommonSwapPropertiesCallback(ElementEntity $element)
-    {
-        $commonSwapProperties = [];
-        $elementProperties = $element->getDataValue('properties');
-        if (isset($elementProperties['action'])) {
-            $commonSwapProperties['action'] = $elementProperties['action'];
-        }
-        if (isset($elementProperties['swapIntoWS'])) {
-            $commonSwapProperties['swapIntoWS'] = $elementProperties['swapIntoWS'];
-        }
-        if (isset($elementProperties['comment'])) {
-            $commonSwapProperties['comment'] = $elementProperties['comment'];
-        }
-        if (isset($elementProperties['notificationAlternativeRecipients'])) {
-            $commonSwapProperties['notificationAlternativeRecipients'] = $elementProperties['notificationAlternativeRecipients'];
-        }
-
-        return $commonSwapProperties;
-    }
-
-    /**
-     * Callback to get the specific properties of a dependent element for staging.
-     *
-     * @param ElementEntity $element
-     * @return array
-     */
-    protected function getElementSetStagePropertiesCallback(ElementEntity $element)
-    {
-        return $this->getCommonSetStagePropertiesCallback($element);
-    }
-
-    /**
-     * Callback to get common properties of dependent elements for staging.
-     *
-     * @param ElementEntity $element
-     * @return array
-     */
-    protected function getCommonSetStagePropertiesCallback(ElementEntity $element)
-    {
-        $commonSetStageProperties = [];
-        $elementProperties = $element->getDataValue('properties');
-        if (isset($elementProperties['stageId'])) {
-            $commonSetStageProperties['stageId'] = $elementProperties['stageId'];
-        }
-        if (isset($elementProperties['comment'])) {
-            $commonSetStageProperties['comment'] = $elementProperties['comment'];
-        }
-        if (isset($elementProperties['action'])) {
-            $commonSetStageProperties['action'] = $elementProperties['action'];
-        }
-        if (isset($elementProperties['notificationAlternativeRecipients'])) {
-            $commonSetStageProperties['notificationAlternativeRecipients'] = $elementProperties['notificationAlternativeRecipients'];
-        }
-        return $commonSetStageProperties;
-    }
-
-    /**
-     * Gets an instance of the depency resolver utility.
-     *
-     * @param string $scope Scope identifier
-     * @return \TYPO3\CMS\Version\Dependency\DependencyResolver
-     */
-    protected function getDependencyUtility($scope)
-    {
-        /** @var $dependency \TYPO3\CMS\Version\Dependency\DependencyResolver */
-        $dependency = GeneralUtility::makeInstance(\TYPO3\CMS\Version\Dependency\DependencyResolver::class);
-        $dependency->setWorkspace($this->getWorkspace());
-        $dependency->setOuterMostParentsRequireReferences(true);
-        if ($this->getScopeData($scope, self::KEY_ElementConstructCallback)) {
-            $dependency->setEventCallback(ElementEntity::EVENT_Construct, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementConstructCallback)));
-        }
-        if ($this->getScopeData($scope, self::KEY_ElementCreateChildReferenceCallback)) {
-            $dependency->setEventCallback(ElementEntity::EVENT_CreateChildReference, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementCreateChildReferenceCallback)));
-        }
-        if ($this->getScopeData($scope, self::KEY_ElementCreateParentReferenceCallback)) {
-            $dependency->setEventCallback(ElementEntity::EVENT_CreateParentReference, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementCreateParentReferenceCallback)));
-        }
-        return $dependency;
-    }
-
-    /**
-     * Constructs the scope settings.
-     * Currently the scopes for swapping/publishing and staging are available.
-     */
-    protected function constructScopes()
-    {
-        $this->scopes = [
-            // settings for publishing and swapping:
-            self::SCOPE_WorkspacesSwap => [
-                // callback functons used to modify the commandMap
-                // + element properties are specific for each element
-                // + common properties are the same for all elements
-                self::KEY_GetElementPropertiesCallback => 'getElementSwapPropertiesCallback',
-                self::KEY_GetCommonPropertiesCallback => 'getCommonSwapPropertiesCallback',
-                // callback function used, when a new element to be checked is added
-                self::KEY_ElementConstructCallback => 'createNewDependentElementCallback',
-                // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
-                self::KEY_ElementCreateChildReferenceCallback => 'createNewDependentElementChildReferenceCallback',
-                self::KEY_ElementCreateParentReferenceCallback => 'createNewDependentElementParentReferenceCallback',
-                // callback function used to fetch the correct record uid on modifying the commandMap
-                self::KEY_UpdateGetIdCallback => 'getElementLiveIdCallback',
-                // setting whether to use the uid of the live record instead of the workspace record
-                self::KEY_TransformDependentElementsToUseLiveId => true
-            ],
-            // settings for modifying the stage:
-            self::SCOPE_WorkspacesSetStage => [
-                // callback functons used to modify the commandMap
-                // + element properties are specific for each element
-                // + common properties are the same for all elements
-                self::KEY_GetElementPropertiesCallback => 'getElementSetStagePropertiesCallback',
-                self::KEY_GetCommonPropertiesCallback => 'getCommonSetStagePropertiesCallback',
-                // callback function used, when a new element to be checked is added
-                self::KEY_ElementConstructCallback => null,
-                // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
-                self::KEY_ElementCreateChildReferenceCallback => 'createNewDependentElementChildReferenceCallback',
-                self::KEY_ElementCreateParentReferenceCallback => 'createNewDependentElementParentReferenceCallback',
-                // callback function used to fetch the correct record uid on modifying the commandMap
-                self::KEY_UpdateGetIdCallback => 'getElementIdCallback',
-                // setting whether to use the uid of the live record instead of the workspace record
-                self::KEY_TransformDependentElementsToUseLiveId => false
-            ],
-            // settings for clearing and flushing:
-            self::SCOPE_WorkspacesClear => [
-                // callback functons used to modify the commandMap
-                // + element properties are specific for each element
-                // + common properties are the same for all elements
-                self::KEY_GetElementPropertiesCallback => null,
-                self::KEY_GetCommonPropertiesCallback => 'getCommonClearPropertiesCallback',
-                // callback function used, when a new element to be checked is added
-                self::KEY_ElementConstructCallback => null,
-                // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
-                self::KEY_ElementCreateChildReferenceCallback => 'createClearDependentElementChildReferenceCallback',
-                self::KEY_ElementCreateParentReferenceCallback => 'createClearDependentElementParentReferenceCallback',
-                // callback function used to fetch the correct record uid on modifying the commandMap
-                self::KEY_UpdateGetIdCallback => 'getElementIdCallback',
-                // setting whether to use the uid of the live record instead of the workspace record
-                self::KEY_TransformDependentElementsToUseLiveId => false
-            ]
-        ];
-    }
-
-    /**
-     * Gets data for a particular scope.
-     *
-     * @throws \RuntimeException
-     * @param string $scope Scope identifier
-     * @param string $key
-     * @return string
-     */
-    protected function getScopeData($scope, $key)
-    {
-        if (!isset($this->scopes[$scope])) {
-            throw new \RuntimeException('Scope "' . $scope . '" is not defined.', 1289342187);
-        }
-        return $this->scopes[$scope][$key];
-    }
-
-    /**
-     * Gets a new callback to be used in the dependency resolver utility.
-     *
-     * @param string $method
-     * @param array $targetArguments
-     * @return \TYPO3\CMS\Version\Dependency\EventCallback
-     */
-    protected function getDependencyCallback($method, array $targetArguments = [])
-    {
-        return GeneralUtility::makeInstance(
-            \TYPO3\CMS\Version\Dependency\EventCallback::class,
-            $this->getElementEntityProcessor(),
-            $method,
-            $targetArguments
-        );
-    }
-
-    /**
-     * Processes a local callback inside this object.
-     *
-     * @param string $method
-     * @param array $callbackArguments
-     * @return mixed
-     */
-    protected function processCallback($method, array $callbackArguments)
-    {
-        return call_user_func_array([$this, $method], $callbackArguments);
-    }
-}
diff --git a/typo3/sysext/version/Classes/Dependency/DependencyEntityFactory.php b/typo3/sysext/version/Classes/Dependency/DependencyEntityFactory.php
deleted file mode 100644 (file)
index 78660e3..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-namespace TYPO3\CMS\Version\Dependency;
-
-/*
- * 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!
- */
-
-/**
- * Object to create and keep track of element or reference entities.
- */
-class DependencyEntityFactory
-{
-    /**
-     * @var array
-     */
-    protected $elements = [];
-
-    /**
-     * @var array
-     */
-    protected $references = [];
-
-    /**
-     * Gets and registers a new element.
-     *
-     * @param string $table
-     * @param int $id
-     * @param array $data (optional)
-     * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency
-     * @return \TYPO3\CMS\Version\Dependency\ElementEntity
-     */
-    public function getElement($table, $id, array $data = [], \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency)
-    {
-        /** @var $element ElementEntity */
-        $element = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Version\Dependency\ElementEntity::class, $table, $id, $data, $dependency);
-        $elementName = $element->__toString();
-        if (!isset($this->elements[$elementName])) {
-            $this->elements[$elementName] = $element;
-        }
-        return $this->elements[$elementName];
-    }
-
-    /**
-     * Gets and registers a new reference.
-     *
-     * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
-     * @param string $field
-     * @return \TYPO3\CMS\Version\Dependency\ReferenceEntity
-     */
-    public function getReference(\TYPO3\CMS\Version\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);
-        }
-        return $this->references[$referenceName][$field];
-    }
-
-    /**
-     * Gets and registers a new reference.
-     *
-     * @param string $table
-     * @param int $id
-     * @param string $field
-     * @param array $data (optional)
-     * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency
-     * @return \TYPO3\CMS\Version\Dependency\ReferenceEntity
-     * @see getElement
-     * @see getReference
-     */
-    public function getReferencedElement($table, $id, $field, array $data = [], \TYPO3\CMS\Version\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/version/Classes/Dependency/DependencyResolver.php
deleted file mode 100644 (file)
index e3dd273..0000000
+++ /dev/null
@@ -1,205 +0,0 @@
-<?php
-namespace TYPO3\CMS\Version\Dependency;
-
-/*
- * 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!
- */
-
-/**
- * Object to handle and determine dependent references of elements.
- */
-class DependencyResolver
-{
-    /**
-     * @var int
-     */
-    protected $workspace = 0;
-
-    /**
-     * @var \TYPO3\CMS\Version\Dependency\DependencyEntityFactory
-     */
-    protected $factory;
-
-    /**
-     * @var array
-     */
-    protected $elements = [];
-
-    /**
-     * @var array
-     */
-    protected $eventCallbacks = [];
-
-    /**
-     * @var bool
-     */
-    protected $outerMostParentsRequireReferences = false;
-
-    /**
-     * @var array
-     */
-    protected $outerMostParents;
-
-    /**
-     * Sets the current workspace.
-     *
-     * @param int $workspace
-     */
-    public function setWorkspace($workspace)
-    {
-        $this->workspace = (int)$workspace;
-    }
-
-    /**
-     * Gets the current workspace.
-     *
-     * @return int
-     */
-    public function getWorkspace()
-    {
-        return $this->workspace;
-    }
-
-    /**
-     * Sets a callback for a particular event.
-     *
-     * @param string $eventName
-     * @param \TYPO3\CMS\Version\Dependency\EventCallback $callback
-     * @return \TYPO3\CMS\Version\Dependency\DependencyResolver
-     */
-    public function setEventCallback($eventName, \TYPO3\CMS\Version\Dependency\EventCallback $callback)
-    {
-        $this->eventCallbacks[$eventName] = $callback;
-        return $this;
-    }
-
-    /**
-     * Executes a registered callback (if any) for a particular event.
-     *
-     * @param string $eventName
-     * @param object $caller
-     * @param array $callerArguments
-     * @return mixed
-     */
-    public function executeEventCallback($eventName, $caller, array $callerArguments = [])
-    {
-        if (isset($this->eventCallbacks[$eventName])) {
-            /** @var $callback \TYPO3\CMS\Version\Dependency\EventCallback */
-            $callback = $this->eventCallbacks[$eventName];
-            return $callback->execute($callerArguments, $caller, $eventName);
-        }
-        return null;
-    }
-
-    /**
-     * Sets the condition that outermost parents required at least one child or parent reference.
-     *
-     * @param bool $outerMostParentsRequireReferences
-     * @return \TYPO3\CMS\Version\Dependency\DependencyResolver
-     */
-    public function setOuterMostParentsRequireReferences($outerMostParentsRequireReferences)
-    {
-        $this->outerMostParentsRequireReferences = (bool)$outerMostParentsRequireReferences;
-        return $this;
-    }
-
-    /**
-     * Adds an element to be checked for dependent references.
-     *
-     * @param string $table
-     * @param int $id
-     * @param array $data
-     * @return \TYPO3\CMS\Version\Dependency\ElementEntity
-     */
-    public function addElement($table, $id, array $data = [])
-    {
-        $element = $this->getFactory()->getElement($table, $id, $data, $this);
-        $elementName = $element->__toString();
-        $this->elements[$elementName] = $element;
-        return $element;
-    }
-
-    /**
-     * Gets the outermost parents that define complete dependent structure each.
-     *
-     * @return array|\TYPO3\CMS\Version\Dependency\ElementEntity[]
-     */
-    public function getOuterMostParents()
-    {
-        if (!isset($this->outerMostParents)) {
-            $this->outerMostParents = [];
-            /** @var $element \TYPO3\CMS\Version\Dependency\ElementEntity */
-            foreach ($this->elements as $element) {
-                $this->processOuterMostParent($element);
-            }
-        }
-        return $this->outerMostParents;
-    }
-
-    /**
-     * Processes and registers the outermost parents accordant to the registered elements.
-     *
-     * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
-     */
-    protected function processOuterMostParent(\TYPO3\CMS\Version\Dependency\ElementEntity $element)
-    {
-        if ($this->outerMostParentsRequireReferences === false || $element->hasReferences()) {
-            $outerMostParent = $element->getOuterMostParent();
-            if ($outerMostParent !== false) {
-                $outerMostParentName = $outerMostParent->__toString();
-                if (!isset($this->outerMostParents[$outerMostParentName])) {
-                    $this->outerMostParents[$outerMostParentName] = $outerMostParent;
-                }
-            }
-        }
-    }
-
-    /**
-     * Gets all nested elements (including the parent) of a particular outermost parent element.
-     *
-     * @throws \RuntimeException
-     * @param \TYPO3\CMS\Version\Dependency\ElementEntity $outerMostParent
-     * @return array
-     */
-    public function getNestedElements(\TYPO3\CMS\Version\Dependency\ElementEntity $outerMostParent)
-    {
-        $outerMostParentName = $outerMostParent->__toString();
-        if (!isset($this->outerMostParents[$outerMostParentName])) {
-            throw new \RuntimeException('Element "' . $outerMostParentName . '" was not detected as outermost parent.', 1289318609);
-        }
-        $nestedStructure = array_merge([$outerMostParentName => $outerMostParent], $outerMostParent->getNestedChildren());
-        return $nestedStructure;
-    }
-
-    /**
-     * Gets the registered elements.
-     *
-     * @return array
-     */
-    public function getElements()
-    {
-        return $this->elements;
-    }
-
-    /**
-     * Gets an instance of the factory to keep track of element or reference entities.
-     *
-     * @return \TYPO3\CMS\Version\Dependency\DependencyEntityFactory
-     */
-    public function getFactory()
-    {
-        if (!isset($this->factory)) {
-            $this->factory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Version\Dependency\DependencyEntityFactory::class);
-        }
-        return $this->factory;
-    }
-}
diff --git a/typo3/sysext/version/Classes/Dependency/ElementEntity.php b/typo3/sysext/version/Classes/Dependency/ElementEntity.php
deleted file mode 100644 (file)
index f6a4811..0000000
+++ /dev/null
@@ -1,443 +0,0 @@
-<?php
-namespace TYPO3\CMS\Version\Dependency;
-
-/*
- * 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 TYPO3\CMS\Core\Database\ConnectionPool;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * Object to hold information on a dependent database element in abstract.
- */
-class ElementEntity
-{
-    const REFERENCES_ChildOf = 'childOf';
-    const REFERENCES_ParentOf = 'parentOf';
-    const EVENT_Construct = 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity::construct';
-    const EVENT_CreateChildReference = 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity::createChildReference';
-    const EVENT_CreateParentReference = 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity::createParentReference';
-    const RESPONSE_Skip = 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity->skip';
-
-    /**
-     * @var bool
-     */
-    protected $invalid = false;
-
-    /**
-     * @var string
-     */
-    protected $table;
-
-    /**
-     * @var int
-     */
-    protected $id;
-
-    /**
-     * @var array
-     */
-    protected $data;
-
-    /**
-     * @var array
-     */
-    protected $record;
-
-    /**
-     * @var \TYPO3\CMS\Version\Dependency\DependencyResolver
-     */
-    protected $dependency;
-
-    /**
-     * @var array
-     */
-    protected $children;
-
-    /**
-     * @var array
-     */
-    protected $parents;
-
-    /**
-     * @var bool
-     */
-    protected $traversingParents = false;
-
-    /**
-     * @var \TYPO3\CMS\Version\Dependency\ElementEntity
-     */
-    protected $outerMostParent;
-
-    /**
-     * @var array
-     */
-    protected $nestedChildren;
-
-    /**
-     * Creates this object.
-     *
-     * @param string $table
-     * @param int $id
-     * @param array $data (optional)
-     * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency
-     */
-    public function __construct($table, $id, array $data = [], \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency)
-    {
-        $this->table = $table;
-        $this->id = (int)$id;
-        $this->data = $data;
-        $this->dependency = $dependency;
-        $this->dependency->executeEventCallback(self::EVENT_Construct, $this);
-    }
-
-    /**
-     * @param bool $invalid
-     */
-    public function setInvalid($invalid)
-    {
-        $this->invalid = (bool)$invalid;
-    }
-
-    /**
-     * @return bool
-     */
-    public function isInvalid()
-    {
-        return $this->invalid;
-    }
-
-    /**
-     * Gets the table.
-     *
-     * @return string
-     */
-    public function getTable()
-    {
-        return $this->table;
-    }
-
-    /**
-     * Gets the id.
-     *
-     * @return int
-     */
-    public function getId()
-    {
-        return $this->id;
-    }
-
-    /**
-     * Sets the id.
-     *
-     * @param int $id
-     */
-    public function setId($id)
-    {
-        $this->id = (int)$id;
-    }
-
-    /**
-     * Gets the data.
-     *
-     * @return array
-     */
-    public function getData()
-    {
-        return $this->data;
-    }
-
-    /**
-     * Gets a value for a particular key from the data.
-     *
-     * @param string $key
-     * @return mixed
-     */
-    public function getDataValue($key)
-    {
-        $result = null;
-        if ($this->hasDataValue($key)) {
-            $result = $this->data[$key];
-        }
-        return $result;
-    }
-
-    /**
-     * Sets a value for a particular key in the data.
-     *
-     * @param string $key
-     * @param mixed $value
-     */
-    public function setDataValue($key, $value)
-    {
-        $this->data[$key] = $value;
-    }
-
-    /**
-     * Determines whether a particular key holds data.
-     *
-     * @param string $key
-     * @return bool
-     */
-    public function hasDataValue($key)
-    {
-        return isset($this->data[$key]);
-    }
-
-    /**
-     * Converts this object for string representation.
-     *
-     * @return string
-     */
-    public function __toString()
-    {
-        return self::getIdentifier($this->table, $this->id);
-    }
-
-    /**
-     * Gets the parent dependency object.
-     *
-     * @return \TYPO3\CMS\Version\Dependency\DependencyResolver
-     */
-    public function getDependency()
-    {
-        return $this->dependency;
-    }
-
-    /**
-     * Gets all child references.
-     *
-     * @return array|ReferenceEntity[]
-     */
-    public function getChildren()
-    {
-        if (!isset($this->children)) {
-            $this->children = [];
-
-            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-                ->getQueryBuilderForTable('sys_refindex');
-
-            $result = $queryBuilder
-                ->select('*')
-                ->from('sys_refindex')
-                ->where(
-                    $queryBuilder->expr()->eq(
-                        'tablename',
-                        $queryBuilder->createNamedParameter($this->table, \PDO::PARAM_STR)
-                    ),
-                    $queryBuilder->expr()->eq(
-                        'recuid',
-                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
-                    ),
-                    $queryBuilder->expr()->eq(
-                        'workspace',
-                        $queryBuilder->createNamedParameter($this->dependency->getWorkspace(), \PDO::PARAM_INT)
-                    )
-                )
-                ->orderBy('sorting')
-                ->execute();
-
-            while ($row = $result->fetch()) {
-                if ($row['ref_table'] !== '_FILE' && $row['ref_table'] !== '_STRING') {
-                    $arguments = [
-                        'table' => $row['ref_table'],
-                        'id' => $row['ref_uid'],
-                        'field' => $row['field'],
-                        'scope' => self::REFERENCES_ChildOf
-                    ];
-
-                    $callbackResponse = $this->dependency->executeEventCallback(
-                        self::EVENT_CreateChildReference,
-                        $this,
-                        $arguments
-                    );
-                    if ($callbackResponse !== self::RESPONSE_Skip) {
-                        $this->children[] = $this->getDependency()->getFactory()->getReferencedElement(
-                            $row['ref_table'],
-                            $row['ref_uid'],
-                            $row['field'],
-                            [],
-                            $this->getDependency()
-                        );
-                    }
-                }
-            }
-        }
-        return $this->children;
-    }
-
-    /**
-     * Gets all parent references.
-     *
-     * @return array|ReferenceEntity[]
-     */
-    public function getParents()
-    {
-        if (!isset($this->parents)) {
-            $this->parents = [];
-
-            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-                ->getQueryBuilderForTable('sys_refindex');
-
-            $result = $queryBuilder
-                ->select('*')
-                ->from('sys_refindex')
-                ->where(
-                    $queryBuilder->expr()->eq(
-                        'deleted',
-                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
-                    ),
-                    $queryBuilder->expr()->eq(
-                        'ref_table',
-                        $queryBuilder->createNamedParameter($this->table, \PDO::PARAM_STR)
-                    ),
-                    $queryBuilder->expr()->eq(
-                        'ref_uid',
-                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
-                    ),
-                    $queryBuilder->expr()->eq(
-                        'workspace',
-                        $queryBuilder->createNamedParameter($this->dependency->getWorkspace(), \PDO::PARAM_INT)
-                    )
-                )
-                ->orderBy('sorting')
-                ->execute();
-
-            while ($row = $result->fetch()) {
-                $arguments = [
-                    'table' => $row['tablename'],
-                    'id' => $row['recuid'],
-                    'field' => $row['field'],
-                    'scope' => self::REFERENCES_ParentOf
-                ];
-                $callbackResponse = $this->dependency->executeEventCallback(
-                    self::EVENT_CreateParentReference,
-                    $this,
-                    $arguments
-                );
-                if ($callbackResponse !== self::RESPONSE_Skip) {
-                    $this->parents[] = $this->getDependency()->getFactory()->getReferencedElement(
-                        $row['tablename'],
-                        $row['recuid'],
-                        $row['field'],
-                        [],
-                        $this->getDependency()
-                    );
-                }
-            }
-        }
-        return $this->parents;
-    }
-
-    /**
-     * Determines whether there are child or parent references.
-     *
-     * @return bool
-     */
-    public function hasReferences()
-    {
-        return !empty($this->getChildren()) || !empty($this->getParents());
-    }
-
-    /**
-     * Gets the outermost parent element.
-     *
-     * @return ElementEntity|bool
-     */
-    public function getOuterMostParent()
-    {
-        if (!isset($this->outerMostParent)) {
-            $parents = $this->getParents();
-            if (empty($parents)) {
-                $this->outerMostParent = $this;
-            } else {
-                $this->outerMostParent = false;
-                /** @var $parent \TYPO3\CMS\Version\Dependency\ReferenceEntity */
-                foreach ($parents as $parent) {
-                    $outerMostParent = $parent->getElement()->getOuterMostParent();
-                    if ($outerMostParent instanceof \TYPO3\CMS\Version\Dependency\ElementEntity) {
-                        $this->outerMostParent = $outerMostParent;
-                        break;
-                    }
-                    if ($outerMostParent === false) {
-                        break;
-                    }
-                }
-            }
-        }
-        return $this->outerMostParent;
-    }
-
-    /**
-     * Gets nested children accumulated.
-     *
-     * @return array|ReferenceEntity[]
-     */
-    public function getNestedChildren()
-    {
-        if (!isset($this->nestedChildren)) {
-            $this->nestedChildren = [];
-            $children = $this->getChildren();
-            /** @var $child \TYPO3\CMS\Version\Dependency\ReferenceEntity */
-            foreach ($children as $child) {
-                $this->nestedChildren = array_merge($this->nestedChildren, [$child->getElement()->__toString() => $child->getElement()], $child->getElement()->getNestedChildren());
-            }
-        }
-        return $this->nestedChildren;
-    }
-
-    /**
-     * Converts the object for string representation.
-     *
-     * @param string $table
-     * @param int $id
-     * @return string
-     */
-    public static function getIdentifier($table, $id)
-    {
-        return $table . ':' . $id;
-    }
-
-    /**
-     * Gets the database record of this element.
-     *
-     * @return array
-     */
-    public function getRecord()
-    {
-        if (empty($this->record['uid']) || (int)$this->record['uid'] !== $this->getId()) {
-            $this->record = [];
-
-            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-                ->getQueryBuilderForTable($this->getTable());
-            $queryBuilder->getRestrictions()->removeAll();
-
-            $row = $queryBuilder
-                ->select('uid', 'pid', 't3ver_wsid', 't3ver_state', 't3ver_oid')
-                ->from($this->getTable())
-                ->where(
-                    $queryBuilder->expr()->eq(
-                        'uid',
-                        $queryBuilder->createNamedParameter($this->getId(), \PDO::PARAM_INT)
-                    )
-                )
-                ->execute()
-                ->fetch();
-
-            if (is_array($row)) {
-                $this->record = $row;
-            }
-        }
-
-        return $this->record;
-    }
-}
diff --git a/typo3/sysext/version/Classes/Dependency/ElementEntityProcessor.php b/typo3/sysext/version/Classes/Dependency/ElementEntityProcessor.php
deleted file mode 100644 (file)
index e65a2df..0000000
+++ /dev/null
@@ -1,226 +0,0 @@
-<?php
-namespace TYPO3\CMS\Version\Dependency;
-
-/*
- * 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 TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Versioning\VersionState;
-
-/**
- * Processor having generic callback methods for element entities
- */
-class ElementEntityProcessor
-{
-    /**
-     * @var int
-     */
-    protected $workspace;
-
-    /**
-     * @var \TYPO3\CMS\Core\DataHandling\DataHandler
-     */
-    protected $dataHandler;
-
-    /**
-     * Sets the current workspace.
-     *
-     * @param int $workspace
-     */
-    public function setWorkspace($workspace)
-    {
-        $this->workspace = (int)$workspace;
-    }
-
-    /**
-     * Gets the current workspace.
-     *
-     * @return int
-     */
-    public function getWorkspace()
-    {
-        return $this->workspace;
-    }
-
-    /**
-     * @return \TYPO3\CMS\Core\DataHandling\DataHandler
-     */
-    public function getDataHandler()
-    {
-        if (!isset($this->dataHandler)) {
-            $this->dataHandler = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
-        }
-        return $this->dataHandler;
-    }
-
-    /**
-     * Transforms dependent elements to use the liveId as array key.
-     *
-     * @param array|ElementEntity[] $elements
-     * @return array
-     */
-    public function transformDependentElementsToUseLiveId(array $elements)
-    {
-        $transformedElements = [];
-        /** @var $element ElementEntity */
-        foreach ($elements as $element) {
-            $elementName = ElementEntity::getIdentifier($element->getTable(), $element->getDataValue('liveId'));
-            $transformedElements[$elementName] = $element;
-        }
-        return $transformedElements;
-    }
-
-    /**
-     * Callback to determine whether a new child reference shall be considered in the dependency resolver utility.
-     *
-     * @param array $callerArguments
-     * @param array $targetArgument
-     * @param ElementEntity $caller
-     * @param string $eventName
-     * @return NULL|string Skip response (if required)
-     */
-    public function createNewDependentElementChildReferenceCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
-    {
-        $fieldConfiguration = BackendUtility::getTcaFieldConfiguration($caller->getTable(), $callerArguments['field']);
-        $inlineFieldType = $this->getDataHandler()->getInlineFieldType($fieldConfiguration);
-        if (!$fieldConfiguration || ($fieldConfiguration['type'] !== 'flex' && $inlineFieldType !== 'field' && $inlineFieldType !== 'list')) {
-            return ElementEntity::RESPONSE_Skip;
-        }
-        return null;
-    }
-
-    /**
-     * Callback to determine whether a new parent reference shall be considered in the dependency resolver utility.
-     *
-     * @param array $callerArguments
-     * @param array $targetArgument
-     * @param \TYPO3\CMS\Version\Dependency\ElementEntity $caller
-     * @param string $eventName
-     * @return NULL|string Skip response (if required)
-     */
-    public function createNewDependentElementParentReferenceCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
-    {
-        $fieldConfiguration = BackendUtility::getTcaFieldConfiguration($callerArguments['table'], $callerArguments['field']);
-        $inlineFieldType = $this->getDataHandler()->getInlineFieldType($fieldConfiguration);
-        if (!$fieldConfiguration || ($fieldConfiguration['type'] !== 'flex' && $inlineFieldType !== 'field' && $inlineFieldType !== 'list')) {
-            return ElementEntity::RESPONSE_Skip;
-        }
-        return null;
-    }
-
-    /**
-     * Callback to determine whether a new child reference shall be considered in the dependency resolver utility.
-     * Only elements that are a delete placeholder are considered.
-     *
-     * @param array $callerArguments
-     * @param array $targetArgument
-     * @param ElementEntity $caller
-     * @param string $eventName
-     * @return NULL|string Skip response (if required)
-     */
-    public function createClearDependentElementChildReferenceCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
-    {
-        $response = $this->createNewDependentElementChildReferenceCallback($callerArguments, $targetArgument, $caller, $eventName);
-        if (empty($response)) {
-            $record = BackendUtility::getRecord($callerArguments['table'], $callerArguments['id']);
-            if (!VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
-                $response = ElementEntity::RESPONSE_Skip;
-            }
-        }
-        return $response;
-    }
-
-    /**
-     * Callback to determine whether a new parent reference shall be considered in the dependency resolver utility.
-     * Only elements that are a delete placeholder are considered.
-     *
-     * @param array $callerArguments
-     * @param array $targetArgument
-     * @param ElementEntity $caller
-     * @param string $eventName
-     * @return NULL|string Skip response (if required)
-     */
-    public function createClearDependentElementParentReferenceCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
-    {
-        $response = $this->createNewDependentElementParentReferenceCallback($callerArguments, $targetArgument, $caller, $eventName);
-        if (empty($response)) {
-            $record = BackendUtility::getRecord($callerArguments['table'], $callerArguments['id']);
-            if (!VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
-                $response = ElementEntity::RESPONSE_Skip;
-            }
-        }
-        return $response;
-    }
-
-    /**
-     * Callback to add additional data to new elements created in the dependency resolver utility.
-     *
-     * @throws \RuntimeException
-     * @param ElementEntity $caller
-     * @param array $callerArguments
-     * @param array $targetArgument
-     * @param string $eventName
-     */
-    public function createNewDependentElementCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
-    {
-        if (!BackendUtility::isTableWorkspaceEnabled($caller->getTable())) {
-            $caller->setInvalid(true);
-            return;
-        }
-
-        $versionRecord = $caller->getRecord();
-        // If version record does not exist, it probably has been deleted (cleared from workspace), this means,
-        // that the reference index still has an old reference pointer, which is "fine" for deleted parents
-        if (empty($versionRecord)) {
-            throw new \RuntimeException(
-                'Element "' . $caller::getIdentifier($caller->getTable(), $caller->getId()) . '" does not exist',
-                1393960943
-            );
-        }
-        // If version is on live workspace, but the pid is negative, mark the record as invalid.
-        // This happens if a change has been discarded (clearWSID) - it will be removed from the command map.
-        if ((int)$versionRecord['t3ver_wsid'] === 0 && (int)$versionRecord['pid'] === -1) {
-            $caller->setDataValue('liveId', $caller->getId());
-            $caller->setInvalid(true);
-            return;
-        }
-        if ($caller->hasDataValue('liveId') === false) {
-            // Set the original uid from the version record
-            if (!empty($versionRecord['t3ver_oid']) && (int)$versionRecord['pid'] === -1 && (int)$versionRecord['t3ver_wsid'] === $this->getWorkspace()) {
-                $caller->setDataValue('liveId', $versionRecord['t3ver_oid']);
-            } elseif ((int)$versionRecord['t3ver_wsid'] === 0 || (int)$versionRecord['pid'] !== -1) {
-                // The current version record is actually a live record or an accordant placeholder for live
-                $caller->setDataValue('liveId', $caller->getId());
-                $versionRecord = BackendUtility::getWorkspaceVersionOfRecord(
-                    $this->getWorkspace(),
-                    $caller->getTable(),
-                    $caller->getId(),
-                    'uid,t3ver_state'
-                );
-                // Set version uid to caller, most likely it's a delete placeholder
-                // for a child record that is not recognized in the reference index
-                if (!empty($versionRecord['uid'])) {
-                    $caller->setId($versionRecord['uid']);
-                } else {
-                    // If no version could be determined, mark record as invalid
-                    // (thus, it will be removed from the command map)
-                    $caller->setInvalid(true);
-                }
-            } else {
-                // In case of an unexpected record state, mark the record as invalid
-                $caller->setInvalid(true);
-            }
-        }
-    }
-}
diff --git a/typo3/sysext/version/Classes/Dependency/EventCallback.php b/typo3/sysext/version/Classes/Dependency/EventCallback.php
deleted file mode 100644 (file)
index 71d7200..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-namespace TYPO3\CMS\Version\Dependency;
-
-/*
- * 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!
- */
-
-/**
- * Object to hold information on a callback to a defined object and method.
- */
-class EventCallback
-{
-    /**
-     * @var object
-     */
-    protected $object;
-
-    /**
-     * @var string
-     */
-    protected $method;
-
-    /**
-     * @var array
-     */
-    protected $targetArguments;
-
-    /**
-     * Creates the objects.
-     *
-     * @param object $object
-     * @param string $method
-     * @param array $targetArguments (optional)
-     */
-    public function __construct($object, $method, array $targetArguments = [])
-    {
-        $this->object = $object;
-        $this->method = $method;
-        $this->targetArguments = $targetArguments;
-        $this->targetArguments['target'] = $object;
-    }
-
-    /**
-     * Executes the callback.
-     *
-     * @param array $callerArguments
-     * @param object $caller
-     * @param string $eventName
-     * @return mixed
-     */
-    public function execute(array $callerArguments = [], $caller, $eventName)
-    {
-        return call_user_func_array([$this->object, $this->method], [$callerArguments, $this->targetArguments, $caller, $eventName]);
-    }
-}
diff --git a/typo3/sysext/version/Classes/Dependency/ReferenceEntity.php b/typo3/sysext/version/Classes/Dependency/ReferenceEntity.php
deleted file mode 100644 (file)
index 47864db..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-namespace TYPO3\CMS\Version\Dependency;
-
-/*
- * 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!
- */
-
-/**
- * Object to hold reference information of a database field and one accordant element.
- */
-class ReferenceEntity
-{
-    /**
-     * @var \TYPO3\CMS\Version\Dependency\ElementEntity
-     */
-    protected $element;
-
-    /**
-     * @var string
-     */
-    protected $field;
-
-    /**
-     * Creates this object.
-     *
-     * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
-     * @param string $field
-     */
-    public function __construct(\TYPO3\CMS\Version\Dependency\ElementEntity $element, $field)
-    {
-        $this->element = $element;
-        $this->field = $field;
-    }
-
-    /**
-     * Gets the elements.
-     *
-     * @return \TYPO3\CMS\Version\Dependency\ElementEntity
-     */
-    public function getElement()
-    {
-        return $this->element;
-    }
-
-    /**
-     * Gets the field.
-     *
-     * @return string
-     */
-    public function getField()
-    {
-        return $this->field;
-    }
-
-    /**
-     * Converts this object for string representation.
-     *
-     * @return string
-     */
-    public function __toString()
-    {
-        return $this->element . '.' . $this->field;
-    }
-}
diff --git a/typo3/sysext/version/Classes/Hook/DataHandlerHook.php b/typo3/sysext/version/Classes/Hook/DataHandlerHook.php
deleted file mode 100644 (file)
index 6554786..0000000
+++ /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 (file)
index 95d36a7..0000000
+++ /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 (file)
index c8ad25d..0000000
+++ /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/Private/Language/locallang_emails.xlf b/typo3/sysext/version/Resources/Private/Language/locallang_emails.xlf
deleted file mode 100644 (file)
index 9f43bf6..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<?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">
-               <header/>
-               <body>
-                       <trans-unit id="subject">
-                               <source>TYPO3 Workspace Note: Stage Change for ###ELEMENT_NAME###</source>
-                       </trans-unit>
-                       <trans-unit id="message" xml:space="preserve">
-                               <source>At the TYPO3 site "###SITE_NAME###" (###SITE_URL###)
-in workspace "###WORKSPACE_TITLE###" (###WORKSPACE_UID###)
-the stage has changed for the element(s) "###RECORD_TITLE###" (###ELEMENT_NAME###) at location "###RECORD_PATH###" in the page tree:
-
-=&gt; ###NEXT_STAGE###
-
-User Comment:
-"###COMMENT###"
-
-State was changed by ###USER_FULLNAME### (username: ###USER_USERNAME###)</source>
-                       </trans-unit>
-               </body>
-       </file>
-</xliff>
diff --git a/typo3/sysext/version/Resources/Public/Icons/Extension.png b/typo3/sysext/version/Resources/Public/Icons/Extension.png
deleted file mode 100644 (file)
index 3a7dfc0..0000000
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 (file)
index a33d342..0000000
+++ /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 (file)
index fd7dda3..0000000
+++ /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 (file)
index 51f9752..0000000
+++ /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 (file)
index 7581229..0000000
+++ /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/workspaces/Classes/DataHandler/CommandMap.php b/typo3/sysext/workspaces/Classes/DataHandler/CommandMap.php
new file mode 100644 (file)
index 0000000..7028053
--- /dev/null
@@ -0,0 +1,808 @@
+<?php
+namespace TYPO3\CMS\Workspaces\DataHandler;
+
+/*
+ * 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 TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Workspaces\Dependency\ElementEntity;
+
+/**
+ * Handles the \TYPO3\CMS\Core\DataHandling\DataHandler command map and is
+ * only used in combination with \TYPO3\CMS\Core\DataHandling\DataHandler
+ */
+class CommandMap
+{
+    const SCOPE_WorkspacesSwap = 'SCOPE_WorkspacesSwap';
+    const SCOPE_WorkspacesSetStage = 'SCOPE_WorkspacesSetStage';
+    const SCOPE_WorkspacesClear = 'SCOPE_WorkspacesClear';
+    const KEY_GetElementPropertiesCallback = 'KEY_GetElementPropertiesCallback';
+    const KEY_GetCommonPropertiesCallback = 'KEY_GetCommonPropertiesCallback';
+    const KEY_ElementConstructCallback = 'KEY_EventConstructCallback';
+    const KEY_ElementCreateChildReferenceCallback = 'KEY_ElementCreateChildReferenceCallback';
+    const KEY_ElementCreateParentReferenceCallback = 'KEY_ElementCreateParentReferenceCallback';
+    const KEY_UpdateGetIdCallback = 'KEY_UpdateGetIdCallback';
+    const KEY_TransformDependentElementsToUseLiveId = 'KEY_TransformDependentElementsToUseLiveId';
+
+    /**
+     * @var \TYPO3\CMS\Workspaces\Hook\DataHandlerHook
+     */
+    protected $parent;
+
+    /**
+     * @var \TYPO3\CMS\Core\DataHandling\DataHandler
+     */
+    protected $tceMain;
+
+    /**
+     * @var array
+     */
+    protected $commandMap = [];
+
+    /**
+     * @var int
+     */
+    protected $workspace;
+
+    /**
+     * @var string
+     */
+    protected $workspacesSwapMode;
+
+    /**
+     * @var string
+     */
+    protected $workspacesChangeStageMode;
+
+    /**
+     * @var array
+     */
+    protected $scopes;
+
+    /**
+     * @var \TYPO3\CMS\Workspaces\Dependency\ElementEntityProcessor
+     */
+    protected $elementEntityProcessor;
+
+    /**
+     * Creates this object.
+     *
+     * @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\Workspaces\Hook\DataHandlerHook $parent, \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain, array $commandMap, $workspace)
+    {
+        $this->setParent($parent);
+        $this->setTceMain($tceMain);
+        $this->set($commandMap);
+        $this->setWorkspace($workspace);
+        $this->setWorkspacesSwapMode($this->getTceMain()->BE_USER->getTSConfigVal('options.workspaces.swapMode'));
+        $this->setWorkspacesChangeStageMode($this->getTceMain()->BE_USER->getTSConfigVal('options.workspaces.changeStageMode'));
+        $this->constructScopes();
+    }
+
+    /**
+     * Gets the command map.
+     *
+     * @return array
+     */
+    public function get()
+    {
+        return $this->commandMap;
+    }
+
+    /**
+     * Sets the command map.
+     *
+     * @param array $commandMap
+     * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap
+     */
+    public function set(array $commandMap)
+    {
+        $this->commandMap = $commandMap;
+        return $this;
+    }
+
+    /**
+     * Gets the parent object.
+     *
+     * @return \TYPO3\CMS\Workspaces\Hook\DataHandlerHook
+     */
+    public function getParent()
+    {
+        return $this->parent;
+    }
+
+    /**
+     * Sets the parent object.
+     *
+     * @param \TYPO3\CMS\Workspaces\Hook\DataHandlerHook $parent
+     * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap
+     */
+    public function setParent(\TYPO3\CMS\Workspaces\Hook\DataHandlerHook $parent)
+    {
+        $this->parent = $parent;
+        return $this;
+    }
+
+    /**
+     * Gets the parent object.
+     *
+     * @return \TYPO3\CMS\Core\DataHandling\DataHandler
+     */
+    public function getTceMain()
+    {
+        return $this->tceMain;
+    }
+
+    /**
+     * Sets the parent object.
+     *
+     * @param \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain
+     * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap
+     */
+    public function setTceMain(\TYPO3\CMS\Core\DataHandling\DataHandler $tceMain)
+    {
+        $this->tceMain = $tceMain;
+        return $this;
+    }
+
+    /**
+     * Sets the current workspace.
+     *
+     * @param int $workspace
+     */
+    public function setWorkspace($workspace)
+    {
+        $this->workspace = (int)$workspace;
+    }
+
+    /**
+     * Gets the current workspace.
+     *
+     * @return int
+     */
+    public function getWorkspace()
+    {
+        return $this->workspace;
+    }
+
+    /**
+     * Sets the workspaces swap mode
+     * (see options.workspaces.swapMode).
+     *
+     * @param string $workspacesSwapMode
+     * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap
+     */
+    public function setWorkspacesSwapMode($workspacesSwapMode)
+    {
+        $this->workspacesSwapMode = (string)$workspacesSwapMode;
+        return $this;
+    }
+
+    /**
+     * Sets the workspaces change stage mode
+     * see options.workspaces.changeStageMode)
+     *
+     * @param string $workspacesChangeStageMode
+     * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap
+     */
+    public function setWorkspacesChangeStageMode($workspacesChangeStageMode)
+    {
+        $this->workspacesChangeStageMode = (string)$workspacesChangeStageMode;
+        return $this;
+    }
+
+    /**
+     * Gets the element entity processor.
+     *
+     * @return \TYPO3\CMS\Workspaces\Dependency\ElementEntityProcessor
+     */
+    protected function getElementEntityProcessor()
+    {
+        if (!isset($this->elementEntityProcessor)) {
+            $this->elementEntityProcessor = GeneralUtility::makeInstance(
+                \TYPO3\CMS\Workspaces\Dependency\ElementEntityProcessor::class
+            );
+            $this->elementEntityProcessor->setWorkspace($this->getWorkspace());
+        }
+        return $this->elementEntityProcessor;
+    }
+
+    /**
+     * Processes the command map.
+     *
+     * @return \TYPO3\CMS\Workspaces\DataHandler\CommandMap
+     */
+    public function process()
+    {
+        $this->resolveWorkspacesSwapDependencies();
+        $this->resolveWorkspacesSetStageDependencies();
+        $this->resolveWorkspacesClearDependencies();
+        return $this;
+    }
+
+    /**
+     * Invokes all items for swapping/publishing with a callback method.
+     *
+     * @param string $callbackMethod
+     * @param array $arguments Optional leading arguments for the callback method
+     */
+    protected function invokeWorkspacesSwapItems($callbackMethod, array $arguments = [])
+    {
+        // Traverses the cmd[] array and fetches the accordant actions:
+        foreach ($this->commandMap as $table => $liveIdCollection) {
+            foreach ($liveIdCollection as $liveId => $commandCollection) {
+                foreach ($commandCollection as $command => $properties) {
+                    if ($command === 'version' && isset($properties['action']) && $properties['action'] === 'swap') {
+                        if (isset($properties['swapWith']) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($properties['swapWith'])) {
+                            call_user_func_array([$this, $callbackMethod], array_merge($arguments, [$table, $liveId, $properties]));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Resolves workspaces related dependencies for swapping/publishing of the command map.
+     * Workspaces records that have children or (relative) parents which are versionized
+     * but not published with this request, are removed from the command map. Otherwise
+     * this would produce hanging record sets and lost references.
+     */
+    protected function resolveWorkspacesSwapDependencies()
+    {
+        $scope = self::SCOPE_WorkspacesSwap;
+        $dependency = $this->getDependencyUtility($scope);
+        if ($this->workspacesSwapMode === 'any' || $this->workspacesSwapMode === 'pages') {
+            $this->invokeWorkspacesSwapItems('applyWorkspacesSwapBehaviour');
+        }
+        $this->invokeWorkspacesSwapItems('addWorkspacesSwapElements', [$dependency]);
+        $this->applyWorkspacesDependencies($dependency, $scope);
+    }
+
+    /**
+     * Applies workspaces behaviour for swapping/publishing and takes care of the swapMode.
+     *
+     * @param string $table
+     * @param int $liveId
+     * @param array $properties
+     */
+    protected function applyWorkspacesSwapBehaviour($table, $liveId, array $properties)
+    {
+        $extendedCommandMap = [];
+        $elementList = [];
+        // Fetch accordant elements if the swapMode is 'any' or 'pages':
+        if ($this->workspacesSwapMode === 'any' || $this->workspacesSwapMode === 'pages' && $table === 'pages') {
+            $elementList = $this->getParent()->findPageElementsForVersionSwap($table, $liveId, $properties['swapWith']);
+        }
+        foreach ($elementList as $elementTable => $elementIdArray) {
+            foreach ($elementIdArray as $elementIds) {
+                $extendedCommandMap[$elementTable][$elementIds[0]]['version'] = array_merge($properties, ['swapWith' => $elementIds[1]]);
+            }
+        }
+        if (!empty($elementList)) {
+            $this->remove($table, $liveId, 'version');
+            $this->mergeToBottom($extendedCommandMap);
+        }
+    }
+
+    /**
+     * Adds workspaces elements for swapping/publishing.
+     *
+     * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency
+     * @param string $table
+     * @param int $liveId
+     * @param 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':
+        if ($this->workspacesSwapMode === 'any' || $this->workspacesSwapMode === 'pages' && $table === 'pages') {
+            $elementList = $this->getParent()->findPageElementsForVersionSwap($table, $liveId, $properties['swapWith']);
+        }
+        foreach ($elementList as $elementTable => $elementIdArray) {
+            foreach ($elementIdArray as $elementIds) {
+                $dependency->addElement($elementTable, $elementIds[1], ['liveId' => $elementIds[0], 'properties' => array_merge($properties, ['swapWith' => $elementIds[1]])]);
+            }
+        }
+        if (empty($elementList)) {
+            $dependency->addElement($table, $properties['swapWith'], ['liveId' => $liveId, 'properties' => $properties]);
+        }
+    }
+
+    /**
+     * Invokes all items for staging with a callback method.
+     *
+     * @param string $callbackMethod
+     * @param array $arguments Optional leading arguments for the callback method
+     */
+    protected function invokeWorkspacesSetStageItems($callbackMethod, array $arguments = [])
+    {
+        // Traverses the cmd[] array and fetches the accordant actions:
+        foreach ($this->commandMap as $table => $versionIdCollection) {
+            foreach ($versionIdCollection as $versionIdList => $commandCollection) {
+                foreach ($commandCollection as $command => $properties) {
+                    if ($command === 'version' && isset($properties['action']) && $properties['action'] === 'setStage') {
+                        if (isset($properties['stageId']) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($properties['stageId'])) {
+                            call_user_func_array([$this, $callbackMethod], array_merge($arguments, [$table, $versionIdList, $properties]));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Resolves workspaces related dependencies for staging of the command map.
+     * Workspaces records that have children or (relative) parents which are versionized
+     * but not staged with this request, are removed from the command map.
+     */
+    protected function resolveWorkspacesSetStageDependencies()
+    {
+        $scope = self::SCOPE_WorkspacesSetStage;
+        $dependency = $this->getDependencyUtility($scope);
+        if ($this->workspacesChangeStageMode === 'any' || $this->workspacesChangeStageMode === 'pages') {
+            $this->invokeWorkspacesSetStageItems('applyWorkspacesSetStageBehaviour');
+        }
+        $this->invokeWorkspacesSetStageItems('explodeSetStage');
+        $this->invokeWorkspacesSetStageItems('addWorkspacesSetStageElements', [$dependency]);
+        $this->applyWorkspacesDependencies($dependency, $scope);
+    }
+
+    /**
+     * Applies workspaces behaviour for staging and takes care of the changeStageMode.
+     *
+     * @param string $table
+     * @param string $versionIdList
+     * @param array $properties
+     */
+    protected function applyWorkspacesSetStageBehaviour($table, $versionIdList, array $properties)
+    {
+        $extendedCommandMap = [];
+        $versionIds = GeneralUtility::trimExplode(',', $versionIdList, true);
+        $elementList = [$table => $versionIds];
+        if ($this->workspacesChangeStageMode === 'any' || $this->workspacesChangeStageMode === 'pages') {
+            if (count($versionIds) === 1) {
+                $workspaceRecord = BackendUtility::getRecord($table, $versionIds[0], 't3ver_wsid');
+                $workspaceId = $workspaceRecord['t3ver_wsid'];
+            } else {
+                $workspaceId = $this->getWorkspace();
+            }
+            if ($table === 'pages') {
+                // Find all elements from the same ws to change stage
+                $livePageIds = $versionIds;
+                $this->getParent()->findRealPageIds($livePageIds);
+                $this->getParent()->findPageElementsForVersionStageChange($livePageIds, $workspaceId, $elementList);
+            } elseif ($this->workspacesChangeStageMode === 'any') {
+                // Find page to change stage:
+                $pageIdList = [];
+                $this->getParent()->findPageIdsForVersionStateChange($table, $versionIds, $workspaceId, $pageIdList, $elementList);
+                // Find other elements from the same ws to change stage:
+                $this->getParent()->findPageElementsForVersionStageChange($pageIdList, $workspaceId, $elementList);
+            }
+        }
+        foreach ($elementList as $elementTable => $elementIds) {
+            foreach ($elementIds as $elementId) {
+                $extendedCommandMap[$elementTable][$elementId]['version'] = $properties;
+            }
+        }
+        $this->remove($table, $versionIds, 'version');
+        $this->mergeToBottom($extendedCommandMap);
+    }
+
+    /**
+     * Adds workspaces elements for staging.
+     *
+     * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency
+     * @param string $table
+     * @param string $versionId
+     * @param array $properties
+     */
+    protected function addWorkspacesSetStageElements(\TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency, $table, $versionId, array $properties)
+    {
+        $dependency->addElement($table, $versionId, ['versionId' => $versionId, 'properties' => $properties]);
+    }
+
+    /**
+     * Resolves workspaces related dependencies for clearing/flushing of the command map.
+     * Workspaces records that have children or (relative) parents which are versionized
+     * but not cleared/flushed with this request, are removed from the command map.
+     */
+    protected function resolveWorkspacesClearDependencies()
+    {
+        $scope = self::SCOPE_WorkspacesClear;
+        $dependency = $this->getDependencyUtility($scope);
+        // Traverses the cmd[] array and fetches the accordant actions:
+        foreach ($this->commandMap as $table => $versionIdCollection) {
+            foreach ($versionIdCollection as $versionId => $commandCollection) {
+                foreach ($commandCollection as $command => $properties) {
+                    if ($command === 'version' && isset($properties['action']) && ($properties['action'] === 'clearWSID' || $properties['action'] === 'flush')) {
+                        $dependency->addElement($table, $versionId, ['versionId' => $versionId, 'properties' => $properties]);
+                    }
+                }
+            }
+        }
+        $this->applyWorkspacesDependencies($dependency, $scope);
+    }
+
+    /**
+     * Explodes id-lists in the command map for staging actions.
+     *
+     * @throws \RuntimeException
+     * @param string $table
+     * @param string $versionIdList
+     * @param array $properties
+     */
+    protected function explodeSetStage($table, $versionIdList, array $properties)
+    {
+        $extractedCommandMap = [];
+        $versionIds = GeneralUtility::trimExplode(',', $versionIdList, true);
+        if (count($versionIds) > 1) {
+            foreach ($versionIds as $versionId) {
+                if (isset($this->commandMap[$table][$versionId]['version'])) {
+                    throw new \RuntimeException('Command map for [' . $table . '][' . $versionId . '][version] was already set.', 1289391048);
+                }
+                $extractedCommandMap[$table][$versionId]['version'] = $properties;
+            }
+            $this->remove($table, $versionIdList, 'version');
+            $this->mergeToBottom($extractedCommandMap);
+        }
+    }
+
+    /**
+     * Applies the workspaces dependencies and removes incomplete structures or automatically
+     * completes them
+     *
+     * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency
+     * @param string $scope
+     */
+    protected function applyWorkspacesDependencies(\TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency, $scope)
+    {
+        $transformDependentElementsToUseLiveId = $this->getScopeData($scope, self::KEY_TransformDependentElementsToUseLiveId);
+        $elementsToBeVersioned = $dependency->getElements();
+        // Use the uid of the live record instead of the workspace record:
+        if ($transformDependentElementsToUseLiveId) {
+            $elementsToBeVersioned = $this->getElementEntityProcessor()->transformDependentElementsToUseLiveId($elementsToBeVersioned);
+        }
+        $outerMostParents = $dependency->getOuterMostParents();
+        /** @var $outerMostParent ElementEntity */
+        foreach ($outerMostParents as $outerMostParent) {
+            $dependentElements = $dependency->getNestedElements($outerMostParent);
+            if ($transformDependentElementsToUseLiveId) {
+                $dependentElements = $this->getElementEntityProcessor()->transformDependentElementsToUseLiveId($dependentElements);
+            }
+            // Gets the difference (intersection) between elements that were submitted by the user
+            // and the evaluation of all dependent records that should be used for this action instead:
+            $intersectingElements = array_intersect_key($dependentElements, $elementsToBeVersioned);
+            if (!empty($intersectingElements)) {
+                $this->update(current($intersectingElements), $dependentElements, $scope);
+            }
+        }
+    }
+
+    /**
+     * Updates the command map accordant to valid structures and takes care of the correct order.
+     *
+     * @param ElementEntity $intersectingElement
+     * @param array $elements
+     * @param string $scope
+     */
+    protected function update(ElementEntity $intersectingElement, array $elements, $scope)
+    {
+        $orderedCommandMap = [];
+        $commonProperties = [];
+        if ($this->getScopeData($scope, self::KEY_GetCommonPropertiesCallback)) {
+            $commonProperties = $this->processCallback($this->getScopeData($scope, self::KEY_GetCommonPropertiesCallback), [$intersectingElement]);
+        }
+        /** @var $element ElementEntity */
+        foreach ($elements as $element) {
+            $table = $element->getTable();
+            $id = $this->processCallback($this->getScopeData($scope, self::KEY_UpdateGetIdCallback), [$element]);
+            $this->remove($table, $id, 'version');
+            if ($element->isInvalid()) {
+                continue;
+            }
+            $orderedCommandMap[$table][$id]['version'] = $commonProperties;
+            if ($this->getScopeData($scope, self::KEY_GetElementPropertiesCallback)) {
+                $orderedCommandMap[$table][$id]['version'] = array_merge($commonProperties, $this->processCallback($this->getScopeData($scope, self::KEY_GetElementPropertiesCallback), [$element]));
+            }
+        }
+        // Ensure that ordered command map is on top of the command map:
+        $this->mergeToTop($orderedCommandMap);
+    }
+
+    /**
+     * Merges command map elements to the top of the current command map..
+     *
+     * @param array $commandMap
+     */
+    protected function mergeToTop(array $commandMap)
+    {
+        \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($commandMap, $this->commandMap);
+        $this->commandMap = $commandMap;
+    }
+
+    /**
+     * Merges command map elements to the bottom of the current command map.
+     *
+     * @param array $commandMap
+     */
+    protected function mergeToBottom(array $commandMap)
+    {
+        \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($this->commandMap, $commandMap);
+    }
+
+    /**
+     * Removes an element from the command map.
+     *
+     * @param string $table
+     * @param string $id
+     * @param string $command (optional)
+     */
+    protected function remove($table, $id, $command = null)
+    {
+        if (is_string($command)) {
+            unset($this->commandMap[$table][$id][$command]);
+        } else {
+            unset($this->commandMap[$table][$id]);
+        }
+    }
+
+    /**
+     * Callback to get the liveId of an dependent element.
+     *
+     * @param ElementEntity $element
+     * @return int
+     */
+    protected function getElementLiveIdCallback(ElementEntity $element)
+    {
+        return $element->getDataValue('liveId');
+    }
+
+    /**
+     * Callback to get the real id of an dependent element.
+     *
+     * @param ElementEntity $element
+     * @return int
+     */
+    protected function getElementIdCallback(ElementEntity $element)
+    {
+        return $element->getId();
+    }
+
+    /**
+     * Callback to get the specific properties of a dependent element for swapping/publishing.
+     *
+     * @param ElementEntity $element
+     * @return array
+     */
+    protected function getElementSwapPropertiesCallback(ElementEntity $element)
+    {
+        return [
+            'swapWith' => $element->getId()
+        ];
+    }
+
+    /**
+     * Callback to get common properties of dependent elements for clearing.
+     *
+     * @param ElementEntity $element
+     * @return array
+     */
+    protected function getCommonClearPropertiesCallback(ElementEntity $element)
+    {
+        $commonSwapProperties = [];
+        $elementProperties = $element->getDataValue('properties');
+        if (isset($elementProperties['action'])) {
+            $commonSwapProperties['action'] = $elementProperties['action'];
+        }
+        return $commonSwapProperties;
+    }
+
+    /**
+     * Callback to get common properties of dependent elements for swapping/publishing.
+     *
+     * @param ElementEntity $element
+     * @return array
+     */
+    protected function getCommonSwapPropertiesCallback(ElementEntity $element)
+    {
+        $commonSwapProperties = [];
+        $elementProperties = $element->getDataValue('properties');
+        if (isset($elementProperties['action'])) {
+            $commonSwapProperties['action'] = $elementProperties['action'];
+        }
+        if (isset($elementProperties['swapIntoWS'])) {
+            $commonSwapProperties['swapIntoWS'] = $elementProperties['swapIntoWS'];
+        }
+        if (isset($elementProperties['comment'])) {
+            $commonSwapProperties['comment'] = $elementProperties['comment'];
+        }
+        if (isset($elementProperties['notificationAlternativeRecipients'])) {
+            $commonSwapProperties['notificationAlternativeRecipients'] = $elementProperties['notificationAlternativeRecipients'];
+        }
+
+        return $commonSwapProperties;
+    }
+
+    /**
+     * Callback to get the specific properties of a dependent element for staging.
+     *
+     * @param ElementEntity $element
+     * @return array
+     */
+    protected function getElementSetStagePropertiesCallback(ElementEntity $element)
+    {
+        return $this->getCommonSetStagePropertiesCallback($element);
+    }
+
+    /**
+     * Callback to get common properties of dependent elements for staging.
+     *
+     * @param ElementEntity $element
+     * @return array
+     */
+    protected function getCommonSetStagePropertiesCallback(ElementEntity $element)
+    {
+        $commonSetStageProperties = [];
+        $elementProperties = $element->getDataValue('properties');
+        if (isset($elementProperties['stageId'])) {
+            $commonSetStageProperties['stageId'] = $elementProperties['stageId'];
+        }
+        if (isset($elementProperties['comment'])) {
+            $commonSetStageProperties['comment'] = $elementProperties['comment'];
+        }
+        if (isset($elementProperties['action'])) {
+            $commonSetStageProperties['action'] = $elementProperties['action'];
+        }
+        if (isset($elementProperties['notificationAlternativeRecipients'])) {
+            $commonSetStageProperties['notificationAlternativeRecipients'] = $elementProperties['notificationAlternativeRecipients'];
+        }
+        return $commonSetStageProperties;
+    }
+
+    /**
+     * Gets an instance of the depency resolver utility.
+     *
+     * @param string $scope Scope identifier
+     * @return \TYPO3\CMS\Workspaces\Dependency\DependencyResolver
+     */
+    protected function getDependencyUtility($scope)
+    {
+        /** @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)) {
+            $dependency->setEventCallback(ElementEntity::EVENT_Construct, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementConstructCallback)));
+        }
+        if ($this->getScopeData($scope, self::KEY_ElementCreateChildReferenceCallback)) {
+            $dependency->setEventCallback(ElementEntity::EVENT_CreateChildReference, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementCreateChildReferenceCallback)));
+        }
+        if ($this->getScopeData($scope, self::KEY_ElementCreateParentReferenceCallback)) {
+            $dependency->setEventCallback(ElementEntity::EVENT_CreateParentReference, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementCreateParentReferenceCallback)));
+        }
+        return $dependency;
+    }
+
+    /**
+     * Constructs the scope settings.
+     * Currently the scopes for swapping/publishing and staging are available.
+     */
+    protected function constructScopes()
+    {
+        $this->scopes = [
+            // settings for publishing and swapping:
+            self::SCOPE_WorkspacesSwap => [
+                // callback functons used to modify the commandMap
+                // + element properties are specific for each element
+                // + common properties are the same for all elements
+                self::KEY_GetElementPropertiesCallback => 'getElementSwapPropertiesCallback',
+                self::KEY_GetCommonPropertiesCallback => 'getCommonSwapPropertiesCallback',
+                // callback function used, when a new element to be checked is added
+                self::KEY_ElementConstructCallback => 'createNewDependentElementCallback',
+                // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
+                self::KEY_ElementCreateChildReferenceCallback => 'createNewDependentElementChildReferenceCallback',
+                self::KEY_ElementCreateParentReferenceCallback => 'createNewDependentElementParentReferenceCallback',
+                // callback function used to fetch the correct record uid on modifying the commandMap
+                self::KEY_UpdateGetIdCallback => 'getElementLiveIdCallback',
+                // setting whether to use the uid of the live record instead of the workspace record
+                self::KEY_TransformDependentElementsToUseLiveId => true
+            ],
+            // settings for modifying the stage:
+            self::SCOPE_WorkspacesSetStage => [
+                // callback functons used to modify the commandMap
+                // + element properties are specific for each element
+                // + common properties are the same for all elements
+                self::KEY_GetElementPropertiesCallback => 'getElementSetStagePropertiesCallback',
+                self::KEY_GetCommonPropertiesCallback => 'getCommonSetStagePropertiesCallback',
+                // callback function used, when a new element to be checked is added
+                self::KEY_ElementConstructCallback => null,
+                // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
+                self::KEY_ElementCreateChildReferenceCallback => 'createNewDependentElementChildReferenceCallback',
+                self::KEY_ElementCreateParentReferenceCallback => 'createNewDependentElementParentReferenceCallback',
+                // callback function used to fetch the correct record uid on modifying the commandMap
+                self::KEY_UpdateGetIdCallback => 'getElementIdCallback',
+                // setting whether to use the uid of the live record instead of the workspace record
+                self::KEY_TransformDependentElementsToUseLiveId => false
+            ],
+            // settings for clearing and flushing:
+            self::SCOPE_WorkspacesClear => [
+                // callback functons used to modify the commandMap
+                // + element properties are specific for each element
+                // + common properties are the same for all elements
+                self::KEY_GetElementPropertiesCallback => null,
+                self::KEY_GetCommonPropertiesCallback => 'getCommonClearPropertiesCallback',
+                // callback function used, when a new element to be checked is added
+                self::KEY_ElementConstructCallback => null,
+                // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
+                self::KEY_ElementCreateChildReferenceCallback => 'createClearDependentElementChildReferenceCallback',
+                self::KEY_ElementCreateParentReferenceCallback => 'createClearDependentElementParentReferenceCallback',
+                // callback function used to fetch the correct record uid on modifying the commandMap
+                self::KEY_UpdateGetIdCallback => 'getElementIdCallback',
+                // setting whether to use the uid of the live record instead of the workspace record
+                self::KEY_TransformDependentElementsToUseLiveId => false
+            ]
+        ];
+    }
+
+    /**
+     * Gets data for a particular scope.
+     *
+     * @throws \RuntimeException
+     * @param string $scope Scope identifier
+     * @param string $key
+     * @return string
+     */
+    protected function getScopeData($scope, $key)
+    {
+        if (!isset($this->scopes[$scope])) {
+            throw new \RuntimeException('Scope "' . $scope . '" is not defined.', 1289342187);
+        }
+        return $this->scopes[$scope][$key];
+    }
+
+    /**
+     * Gets a new callback to be used in the dependency resolver utility.
+     *
+     * @param string $method
+     * @param array $targetArguments
+     * @return \TYPO3\CMS\Workspaces\Dependency\EventCallback
+     */
+    protected function getDependencyCallback($method, array $targetArguments = [])
+    {
+        return GeneralUtility::makeInstance(
+            \TYPO3\CMS\Workspaces\Dependency\EventCallback::class,
+            $this->getElementEntityProcessor(),
+            $method,
+            $targetArguments
+        );
+    }
+
+    /**
+     * Processes a local callback inside this object.
+     *
+     * @param string $method
+     * @param array $callbackArguments
+     * @return mixed
+     */
+    protected function processCallback($method, array $callbackArguments)
+    {
+        return call_user_func_array([$this, $method], $callbackArguments);
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Dependency/DependencyEntityFactory.php b/typo3/sysext/workspaces/Classes/Dependency/DependencyEntityFactory.php
new file mode 100644 (file)
index 0000000..95e90ca
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Dependency;
+
+/*
+ * 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!
+ */
+
+/**
+ * Object to create and keep track of element or reference entities.
+ */
+class DependencyEntityFactory
+{
+    /**
+     * @var array
+     */
+    protected $elements = [];
+
+    /**
+     * @var array
+     */
+    protected $references = [];
+
+    /**
+     * Gets and registers a new element.
+     *
+     * @param string $table
+     * @param int $id
+     * @param array $data (optional)
+     * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency
+     * @return \TYPO3\CMS\Workspaces\Dependency\ElementEntity
+     */
+    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\Workspaces\Dependency\ElementEntity::class, $table, $id, $data, $dependency);
+        $elementName = $element->__toString();
+        if (!isset($this->elements[$elementName])) {
+            $this->elements[$elementName] = $element;
+        }
+        return $this->elements[$elementName];
+    }
+
+    /**
+     * Gets and registers a new reference.
+     *
+     * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $element
+     * @param string $field
+     * @return \TYPO3\CMS\Workspaces\Dependency\ReferenceEntity
+     */
+    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\Workspaces\Dependency\ReferenceEntity::class, $element, $field);
+        }
+        return $this->references[$referenceName][$field];
+    }
+
+    /**
+     * Gets and registers a new reference.
+     *
+     * @param string $table
+     * @param int $id
+     * @param string $field
+     * @param array $data (optional)
+     * @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\Workspaces\Dependency\DependencyResolver $dependency)
+    {
+        return $this->getReference($this->getElement($table, $id, $data, $dependency), $field);
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Dependency/DependencyResolver.php b/typo3/sysext/workspaces/Classes/Dependency/DependencyResolver.php
new file mode 100644 (file)
index 0000000..01eaf93
--- /dev/null
@@ -0,0 +1,205 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Dependency;
+
+/*
+ * 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!
+ */
+
+/**
+ * Object to handle and determine dependent references of elements.
+ */
+class DependencyResolver
+{
+    /**
+     * @var int
+     */
+    protected $workspace = 0;
+
+    /**
+     * @var \TYPO3\CMS\Workspaces\Dependency\DependencyEntityFactory
+     */
+    protected $factory;
+
+    /**
+     * @var array
+     */
+    protected $elements = [];
+
+    /**
+     * @var array
+     */
+    protected $eventCallbacks = [];
+
+    /**
+     * @var bool
+     */
+    protected $outerMostParentsRequireReferences = false;
+
+    /**
+     * @var array
+     */
+    protected $outerMostParents;
+
+    /**
+     * Sets the current workspace.
+     *
+     * @param int $workspace
+     */
+    public function setWorkspace($workspace)
+    {
+        $this->workspace = (int)$workspace;
+    }
+
+    /**
+     * Gets the current workspace.
+     *
+     * @return int
+     */
+    public function getWorkspace()
+    {
+        return $this->workspace;
+    }
+
+    /**
+     * Sets a callback for a particular event.
+     *
+     * @param string $eventName
+     * @param \TYPO3\CMS\Workspaces\Dependency\EventCallback $callback
+     * @return \TYPO3\CMS\Workspaces\Dependency\DependencyResolver
+     */
+    public function setEventCallback($eventName, \TYPO3\CMS\Workspaces\Dependency\EventCallback $callback)
+    {
+        $this->eventCallbacks[$eventName] = $callback;
+        return $this;
+    }
+
+    /**
+     * Executes a registered callback (if any) for a particular event.
+     *
+     * @param string $eventName
+     * @param object $caller
+     * @param array $callerArguments
+     * @return mixed
+     */
+    public function executeEventCallback($eventName, $caller, array $callerArguments = [])
+    {
+        if (isset($this->eventCallbacks[$eventName])) {
+            /** @var $callback \TYPO3\CMS\Workspaces\Dependency\EventCallback */
+            $callback = $this->eventCallbacks[$eventName];
+            return $callback->execute($callerArguments, $caller, $eventName);
+        }
+        return null;
+    }
+
+    /**
+     * Sets the condition that outermost parents required at least one child or parent reference.
+     *
+     * @param bool $outerMostParentsRequireReferences
+     * @return \TYPO3\CMS\Workspaces\Dependency\DependencyResolver
+     */
+    public function setOuterMostParentsRequireReferences($outerMostParentsRequireReferences)
+    {
+        $this->outerMostParentsRequireReferences = (bool)$outerMostParentsRequireReferences;
+        return $this;
+    }
+
+    /**
+     * Adds an element to be checked for dependent references.
+     *
+     * @param string $table
+     * @param int $id
+     * @param array $data
+     * @return \TYPO3\CMS\Workspaces\Dependency\ElementEntity
+     */
+    public function addElement($table, $id, array $data = [])
+    {
+        $element = $this->getFactory()->getElement($table, $id, $data, $this);
+        $elementName = $element->__toString();
+        $this->elements[$elementName] = $element;
+        return $element;
+    }
+
+    /**
+     * Gets the outermost parents that define complete dependent structure each.
+     *
+     * @return array|\TYPO3\CMS\Workspaces\Dependency\ElementEntity[]
+     */
+    public function getOuterMostParents()
+    {
+        if (!isset($this->outerMostParents)) {
+            $this->outerMostParents = [];
+            /** @var $element \TYPO3\CMS\Workspaces\Dependency\ElementEntity */
+            foreach ($this->elements as $element) {
+                $this->processOuterMostParent($element);
+            }
+        }
+        return $this->outerMostParents;
+    }
+
+    /**
+     * Processes and registers the outermost parents accordant to the registered elements.
+     *
+     * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $element
+     */
+    protected function processOuterMostParent(\TYPO3\CMS\Workspaces\Dependency\ElementEntity $element)
+    {
+        if ($this->outerMostParentsRequireReferences === false || $element->hasReferences()) {
+            $outerMostParent = $element->getOuterMostParent();
+            if ($outerMostParent !== false) {
+                $outerMostParentName = $outerMostParent->__toString();
+                if (!isset($this->outerMostParents[$outerMostParentName])) {
+                    $this->outerMostParents[$outerMostParentName] = $outerMostParent;
+                }
+            }
+        }
+    }
+
+    /**
+     * Gets all nested elements (including the parent) of a particular outermost parent element.
+     *
+     * @throws \RuntimeException
+     * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $outerMostParent
+     * @return array
+     */
+    public function getNestedElements(\TYPO3\CMS\Workspaces\Dependency\ElementEntity $outerMostParent)
+    {
+        $outerMostParentName = $outerMostParent->__toString();
+        if (!isset($this->outerMostParents[$outerMostParentName])) {
+            throw new \RuntimeException('Element "' . $outerMostParentName . '" was not detected as outermost parent.', 1289318609);
+        }
+        $nestedStructure = array_merge([$outerMostParentName => $outerMostParent], $outerMostParent->getNestedChildren());
+        return $nestedStructure;
+    }
+
+    /**
+     * Gets the registered elements.
+     *
+     * @return array
+     */
+    public function getElements()
+    {
+        return $this->elements;
+    }
+
+    /**
+     * Gets an instance of the factory to keep track of element or reference entities.
+     *
+     * @return \TYPO3\CMS\Workspaces\Dependency\DependencyEntityFactory
+     */
+    public function getFactory()
+    {
+        if (!isset($this->factory)) {
+            $this->factory = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Dependency\DependencyEntityFactory::class);
+        }
+        return $this->factory;
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Dependency/ElementEntity.php b/typo3/sysext/workspaces/Classes/Dependency/ElementEntity.php
new file mode 100644 (file)
index 0000000..b3e40fe
--- /dev/null
@@ -0,0 +1,443 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Dependency;
+
+/*
+ * 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 TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Object to hold information on a dependent database element in abstract.
+ */
+class ElementEntity
+{
+    const REFERENCES_ChildOf = 'childOf';
+    const REFERENCES_ParentOf = 'parentOf';
+    const EVENT_Construct = 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity::construct';
+    const EVENT_CreateChildReference = 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity::createChildReference';
+    const EVENT_CreateParentReference = 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity::createParentReference';
+    const RESPONSE_Skip = 'TYPO3\\CMS\\Version\\Dependency\\ElementEntity->skip';
+
+    /**
+     * @var bool
+     */
+    protected $invalid = false;
+
+    /**
+     * @var string
+     */
+    protected $table;
+
+    /**
+     * @var int
+     */
+    protected $id;
+
+    /**
+     * @var array
+     */
+    protected $data;
+
+    /**
+     * @var array
+     */
+    protected $record;
+
+    /**
+     * @var \TYPO3\CMS\Workspaces\Dependency\DependencyResolver
+     */
+    protected $dependency;
+
+    /**
+     * @var array
+     */
+    protected $children;
+
+    /**
+     * @var array
+     */
+    protected $parents;
+
+    /**
+     * @var bool
+     */
+    protected $traversingParents = false;
+
+    /**
+     * @var \TYPO3\CMS\Workspaces\Dependency\ElementEntity
+     */
+    protected $outerMostParent;
+
+    /**
+     * @var array
+     */
+    protected $nestedChildren;
+
+    /**
+     * Creates this object.
+     *
+     * @param string $table
+     * @param int $id
+     * @param array $data (optional)
+     * @param \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency
+     */
+    public function __construct($table, $id, array $data = [], \TYPO3\CMS\Workspaces\Dependency\DependencyResolver $dependency)
+    {
+        $this->table = $table;
+        $this->id = (int)$id;
+        $this->data = $data;
+        $this->dependency = $dependency;
+        $this->dependency->executeEventCallback(self::EVENT_Construct, $this);
+    }
+
+    /**
+     * @param bool $invalid
+     */
+    public function setInvalid($invalid)
+    {
+        $this->invalid = (bool)$invalid;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isInvalid()
+    {
+        return $this->invalid;
+    }
+
+    /**
+     * Gets the table.
+     *
+     * @return string
+     */
+    public function getTable()
+    {
+        return $this->table;
+    }
+
+    /**
+     * Gets the id.
+     *
+     * @return int
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Sets the id.
+     *
+     * @param int $id
+     */
+    public function setId($id)
+    {
+        $this->id = (int)$id;
+    }
+
+    /**
+     * Gets the data.
+     *
+     * @return array
+     */
+    public function getData()
+    {
+        return $this->data;
+    }
+
+    /**
+     * Gets a value for a particular key from the data.
+     *
+     * @param string $key
+     * @return mixed
+     */
+    public function getDataValue($key)
+    {
+        $result = null;
+        if ($this->hasDataValue($key)) {
+            $result = $this->data[$key];
+        }
+        return $result;
+    }
+
+    /**
+     * Sets a value for a particular key in the data.
+     *
+     * @param string $key
+     * @param mixed $value
+     */
+    public function setDataValue($key, $value)
+    {
+        $this->data[$key] = $value;
+    }
+
+    /**
+     * Determines whether a particular key holds data.
+     *
+     * @param string $key
+     * @return bool
+     */
+    public function hasDataValue($key)
+    {
+        return isset($this->data[$key]);
+    }
+
+    /**
+     * Converts this object for string representation.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return self::getIdentifier($this->table, $this->id);
+    }
+
+    /**
+     * Gets the parent dependency object.
+     *
+     * @return \TYPO3\CMS\Workspaces\Dependency\DependencyResolver
+     */
+    public function getDependency()
+    {
+        return $this->dependency;
+    }
+
+    /**
+     * Gets all child references.
+     *
+     * @return array|ReferenceEntity[]
+     */
+    public function getChildren()
+    {
+        if (!isset($this->children)) {
+            $this->children = [];
+
+            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getQueryBuilderForTable('sys_refindex');
+
+            $result = $queryBuilder
+                ->select('*')
+                ->from('sys_refindex')
+                ->where(
+                    $queryBuilder->expr()->eq(
+                        'tablename',
+                        $queryBuilder->createNamedParameter($this->table, \PDO::PARAM_STR)
+                    ),
+                    $queryBuilder->expr()->eq(
+                        'recuid',
+                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
+                    ),
+                    $queryBuilder->expr()->eq(
+                        'workspace',
+                        $queryBuilder->createNamedParameter($this->dependency->getWorkspace(), \PDO::PARAM_INT)
+                    )
+                )
+                ->orderBy('sorting')
+                ->execute();
+
+            while ($row = $result->fetch()) {
+                if ($row['ref_table'] !== '_FILE' && $row['ref_table'] !== '_STRING') {
+                    $arguments = [
+                        'table' => $row['ref_table'],
+                        'id' => $row['ref_uid'],
+                        'field' => $row['field'],
+                        'scope' => self::REFERENCES_ChildOf
+                    ];
+
+                    $callbackResponse = $this->dependency->executeEventCallback(
+                        self::EVENT_CreateChildReference,
+                        $this,
+                        $arguments
+                    );
+                    if ($callbackResponse !== self::RESPONSE_Skip) {
+                        $this->children[] = $this->getDependency()->getFactory()->getReferencedElement(
+                            $row['ref_table'],
+                            $row['ref_uid'],
+                            $row['field'],
+                            [],
+                            $this->getDependency()
+                        );
+                    }
+                }
+            }
+        }
+        return $this->children;
+    }
+
+    /**
+     * Gets all parent references.
+     *
+     * @return array|ReferenceEntity[]
+     */
+    public function getParents()
+    {
+        if (!isset($this->parents)) {
+            $this->parents = [];
+
+            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getQueryBuilderForTable('sys_refindex');
+
+            $result = $queryBuilder
+                ->select('*')
+                ->from('sys_refindex')
+                ->where(
+                    $queryBuilder->expr()->eq(
+                        'deleted',
+                        $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
+                    ),
+                    $queryBuilder->expr()->eq(
+                        'ref_table',
+                        $queryBuilder->createNamedParameter($this->table, \PDO::PARAM_STR)
+                    ),
+                    $queryBuilder->expr()->eq(
+                        'ref_uid',
+                        $queryBuilder->createNamedParameter($this->id, \PDO::PARAM_INT)
+                    ),
+                    $queryBuilder->expr()->eq(
+                        'workspace',
+                        $queryBuilder->createNamedParameter($this->dependency->getWorkspace(), \PDO::PARAM_INT)
+                    )
+                )
+                ->orderBy('sorting')
+                ->execute();
+
+            while ($row = $result->fetch()) {
+                $arguments = [
+                    'table' => $row['tablename'],
+                    'id' => $row['recuid'],
+                    'field' => $row['field'],
+                    'scope' => self::REFERENCES_ParentOf
+                ];
+                $callbackResponse = $this->dependency->executeEventCallback(
+                    self::EVENT_CreateParentReference,
+                    $this,
+                    $arguments
+                );
+                if ($callbackResponse !== self::RESPONSE_Skip) {
+                    $this->parents[] = $this->getDependency()->getFactory()->getReferencedElement(
+                        $row['tablename'],
+                        $row['recuid'],
+                        $row['field'],
+                        [],
+                        $this->getDependency()
+                    );
+                }
+            }
+        }
+        return $this->parents;
+    }
+
+    /**
+     * Determines whether there are child or parent references.
+     *
+     * @return bool
+     */
+    public function hasReferences()
+    {
+        return !empty($this->getChildren()) || !empty($this->getParents());
+    }
+
+    /**
+     * Gets the outermost parent element.
+     *
+     * @return ElementEntity|bool
+     */
+    public function getOuterMostParent()
+    {
+        if (!isset($this->outerMostParent)) {
+            $parents = $this->getParents();
+            if (empty($parents)) {
+                $this->outerMostParent = $this;
+            } else {
+                $this->outerMostParent = false;
+                /** @var $parent \TYPO3\CMS\Workspaces\Dependency\ReferenceEntity */
+                foreach ($parents as $parent) {
+                    $outerMostParent = $parent->getElement()->getOuterMostParent();
+                    if ($outerMostParent instanceof \TYPO3\CMS\Workspaces\Dependency\ElementEntity) {
+                        $this->outerMostParent = $outerMostParent;
+                        break;
+                    }
+                    if ($outerMostParent === false) {
+                        break;
+                    }
+                }
+            }
+        }
+        return $this->outerMostParent;
+    }
+
+    /**
+     * Gets nested children accumulated.
+     *
+     * @return array|ReferenceEntity[]
+     */
+    public function getNestedChildren()
+    {
+        if (!isset($this->nestedChildren)) {
+            $this->nestedChildren = [];
+            $children = $this->getChildren();
+            /** @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());
+            }
+        }
+        return $this->nestedChildren;
+    }
+
+    /**
+     * Converts the object for string representation.
+     *
+     * @param string $table
+     * @param int $id
+     * @return string
+     */
+    public static function getIdentifier($table, $id)
+    {
+        return $table . ':' . $id;
+    }
+
+    /**
+     * Gets the database record of this element.
+     *
+     * @return array
+     */
+    public function getRecord()
+    {
+        if (empty($this->record['uid']) || (int)$this->record['uid'] !== $this->getId()) {
+            $this->record = [];
+
+            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
+                ->getQueryBuilderForTable($this->getTable());
+            $queryBuilder->getRestrictions()->removeAll();
+
+            $row = $queryBuilder
+                ->select('uid', 'pid', 't3ver_wsid', 't3ver_state', 't3ver_oid')
+                ->from($this->getTable())
+                ->where(
+                    $queryBuilder->expr()->eq(
+                        'uid',
+                        $queryBuilder->createNamedParameter($this->getId(), \PDO::PARAM_INT)
+                    )
+                )
+                ->execute()
+                ->fetch();
+
+            if (is_array($row)) {
+                $this->record = $row;
+            }
+        }
+
+        return $this->record;
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Dependency/ElementEntityProcessor.php b/typo3/sysext/workspaces/Classes/Dependency/ElementEntityProcessor.php
new file mode 100644 (file)
index 0000000..0155440
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Dependency;
+
+/*
+ * 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 TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Versioning\VersionState;
+
+/**
+ * Processor having generic callback methods for element entities
+ */
+class ElementEntityProcessor
+{
+    /**
+     * @var int
+     */
+    protected $workspace;
+
+    /**
+     * @var \TYPO3\CMS\Core\DataHandling\DataHandler
+     */
+    protected $dataHandler;
+
+    /**
+     * Sets the current workspace.
+     *
+     * @param int $workspace
+     */
+    public function setWorkspace($workspace)
+    {
+        $this->workspace = (int)$workspace;
+    }
+
+    /**
+     * Gets the current workspace.
+     *
+     * @return int
+     */
+    public function getWorkspace()
+    {
+        return $this->workspace;
+    }
+
+    /**
+     * @return \TYPO3\CMS\Core\DataHandling\DataHandler
+     */
+    public function getDataHandler()
+    {
+        if (!isset($this->dataHandler)) {
+            $this->dataHandler = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
+        }
+        return $this->dataHandler;
+    }
+
+    /**
+     * Transforms dependent elements to use the liveId as array key.
+     *
+     * @param array|ElementEntity[] $elements
+     * @return array
+     */
+    public function transformDependentElementsToUseLiveId(array $elements)
+    {
+        $transformedElements = [];
+        /** @var $element ElementEntity */
+        foreach ($elements as $element) {
+            $elementName = ElementEntity::getIdentifier($element->getTable(), $element->getDataValue('liveId'));
+            $transformedElements[$elementName] = $element;
+        }
+        return $transformedElements;
+    }
+
+    /**
+     * Callback to determine whether a new child reference shall be considered in the dependency resolver utility.
+     *
+     * @param array $callerArguments
+     * @param array $targetArgument
+     * @param ElementEntity $caller
+     * @param string $eventName
+     * @return NULL|string Skip response (if required)
+     */
+    public function createNewDependentElementChildReferenceCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
+    {
+        $fieldConfiguration = BackendUtility::getTcaFieldConfiguration($caller->getTable(), $callerArguments['field']);
+        $inlineFieldType = $this->getDataHandler()->getInlineFieldType($fieldConfiguration);
+        if (!$fieldConfiguration || ($fieldConfiguration['type'] !== 'flex' && $inlineFieldType !== 'field' && $inlineFieldType !== 'list')) {
+            return ElementEntity::RESPONSE_Skip;
+        }
+        return null;
+    }
+
+    /**
+     * Callback to determine whether a new parent reference shall be considered in the dependency resolver utility.
+     *
+     * @param array $callerArguments
+     * @param array $targetArgument
+     * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $caller
+     * @param string $eventName
+     * @return NULL|string Skip response (if required)
+     */
+    public function createNewDependentElementParentReferenceCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
+    {
+        $fieldConfiguration = BackendUtility::getTcaFieldConfiguration($callerArguments['table'], $callerArguments['field']);
+        $inlineFieldType = $this->getDataHandler()->getInlineFieldType($fieldConfiguration);
+        if (!$fieldConfiguration || ($fieldConfiguration['type'] !== 'flex' && $inlineFieldType !== 'field' && $inlineFieldType !== 'list')) {
+            return ElementEntity::RESPONSE_Skip;
+        }
+        return null;
+    }
+
+    /**
+     * Callback to determine whether a new child reference shall be considered in the dependency resolver utility.
+     * Only elements that are a delete placeholder are considered.
+     *
+     * @param array $callerArguments
+     * @param array $targetArgument
+     * @param ElementEntity $caller
+     * @param string $eventName
+     * @return NULL|string Skip response (if required)
+     */
+    public function createClearDependentElementChildReferenceCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
+    {
+        $response = $this->createNewDependentElementChildReferenceCallback($callerArguments, $targetArgument, $caller, $eventName);
+        if (empty($response)) {
+            $record = BackendUtility::getRecord($callerArguments['table'], $callerArguments['id']);
+            if (!VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
+                $response = ElementEntity::RESPONSE_Skip;
+            }
+        }
+        return $response;
+    }
+
+    /**
+     * Callback to determine whether a new parent reference shall be considered in the dependency resolver utility.
+     * Only elements that are a delete placeholder are considered.
+     *
+     * @param array $callerArguments
+     * @param array $targetArgument
+     * @param ElementEntity $caller
+     * @param string $eventName
+     * @return NULL|string Skip response (if required)
+     */
+    public function createClearDependentElementParentReferenceCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
+    {
+        $response = $this->createNewDependentElementParentReferenceCallback($callerArguments, $targetArgument, $caller, $eventName);
+        if (empty($response)) {
+            $record = BackendUtility::getRecord($callerArguments['table'], $callerArguments['id']);
+            if (!VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
+                $response = ElementEntity::RESPONSE_Skip;
+            }
+        }
+        return $response;
+    }
+
+    /**
+     * Callback to add additional data to new elements created in the dependency resolver utility.
+     *
+     * @throws \RuntimeException
+     * @param ElementEntity $caller
+     * @param array $callerArguments
+     * @param array $targetArgument
+     * @param string $eventName
+     */
+    public function createNewDependentElementCallback(array $callerArguments, array $targetArgument, ElementEntity $caller, $eventName)
+    {
+        if (!BackendUtility::isTableWorkspaceEnabled($caller->getTable())) {
+            $caller->setInvalid(true);
+            return;
+        }
+
+        $versionRecord = $caller->getRecord();
+        // If version record does not exist, it probably has been deleted (cleared from workspace), this means,
+        // that the reference index still has an old reference pointer, which is "fine" for deleted parents
+        if (empty($versionRecord)) {
+            throw new \RuntimeException(
+                'Element "' . $caller::getIdentifier($caller->getTable(), $caller->getId()) . '" does not exist',
+                1393960943
+            );
+        }
+        // If version is on live workspace, but the pid is negative, mark the record as invalid.
+        // This happens if a change has been discarded (clearWSID) - it will be removed from the command map.
+        if ((int)$versionRecord['t3ver_wsid'] === 0 && (int)$versionRecord['pid'] === -1) {
+            $caller->setDataValue('liveId', $caller->getId());
+            $caller->setInvalid(true);
+            return;
+        }
+        if ($caller->hasDataValue('liveId') === false) {
+            // Set the original uid from the version record
+            if (!empty($versionRecord['t3ver_oid']) && (int)$versionRecord['pid'] === -1 && (int)$versionRecord['t3ver_wsid'] === $this->getWorkspace()) {
+                $caller->setDataValue('liveId', $versionRecord['t3ver_oid']);
+            } elseif ((int)$versionRecord['t3ver_wsid'] === 0 || (int)$versionRecord['pid'] !== -1) {
+                // The current version record is actually a live record or an accordant placeholder for live
+                $caller->setDataValue('liveId', $caller->getId());
+                $versionRecord = BackendUtility::getWorkspaceVersionOfRecord(
+                    $this->getWorkspace(),
+                    $caller->getTable(),
+                    $caller->getId(),
+                    'uid,t3ver_state'
+                );
+                // Set version uid to caller, most likely it's a delete placeholder
+                // for a child record that is not recognized in the reference index
+                if (!empty($versionRecord['uid'])) {
+                    $caller->setId($versionRecord['uid']);
+                } else {
+                    // If no version could be determined, mark record as invalid
+                    // (thus, it will be removed from the command map)
+                    $caller->setInvalid(true);
+                }
+            } else {
+                // In case of an unexpected record state, mark the record as invalid
+                $caller->setInvalid(true);
+            }
+        }
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Dependency/EventCallback.php b/typo3/sysext/workspaces/Classes/Dependency/EventCallback.php
new file mode 100644 (file)
index 0000000..096fd74
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Dependency;
+
+/*
+ * 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!
+ */
+
+/**
+ * Object to hold information on a callback to a defined object and method.
+ */
+class EventCallback
+{
+    /**
+     * @var object
+     */
+    protected $object;
+
+    /**
+     * @var string
+     */
+    protected $method;
+
+    /**
+     * @var array
+     */
+    protected $targetArguments;
+
+    /**
+     * Creates the objects.
+     *
+     * @param object $object
+     * @param string $method
+     * @param array $targetArguments (optional)
+     */
+    public function __construct($object, $method, array $targetArguments = [])
+    {
+        $this->object = $object;
+        $this->method = $method;
+        $this->targetArguments = $targetArguments;
+        $this->targetArguments['target'] = $object;
+    }
+
+    /**
+     * Executes the callback.
+     *
+     * @param array $callerArguments
+     * @param object $caller
+     * @param string $eventName
+     * @return mixed
+     */
+    public function execute(array $callerArguments = [], $caller, $eventName)
+    {
+        return call_user_func_array([$this->object, $this->method], [$callerArguments, $this->targetArguments, $caller, $eventName]);
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Dependency/ReferenceEntity.php b/typo3/sysext/workspaces/Classes/Dependency/ReferenceEntity.php
new file mode 100644 (file)
index 0000000..e2256ef
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Dependency;
+
+/*
+ * 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!
+ */
+
+/**
+ * Object to hold reference information of a database field and one accordant element.
+ */
+class ReferenceEntity
+{
+    /**
+     * @var \TYPO3\CMS\Workspaces\Dependency\ElementEntity
+     */
+    protected $element;
+
+    /**
+     * @var string
+     */
+    protected $field;
+
+    /**
+     * Creates this object.
+     *
+     * @param \TYPO3\CMS\Workspaces\Dependency\ElementEntity $element
+     * @param string $field
+     */
+    public function __construct(\TYPO3\CMS\Workspaces\Dependency\ElementEntity $element, $field)
+    {
+        $this->element = $element;
+        $this->field = $field;
+    }
+
+    /**
+     * Gets the elements.
+     *
+     * @return \TYPO3\CMS\Workspaces\Dependency\ElementEntity
+     */
+    public function getElement()
+    {
+        return $this->element;
+    }
+
+    /**
+     * Gets the field.
+     *
+     * @return string
+     */
+    public function getField()
+    {
+        return $this->field;
+    }
+
+    /**
+     * Converts this object for string representation.
+     *
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->element . '.' . $this->field;
+    }
+}
index a77acd4..2819d81 100644 (file)
@@ -14,19 +14,268 @@ 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
      * as orphan records.
@@ -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) {
+