[!!!][TASK] Remove ExtDirect from Workspaces 94/50294/15
authorFrank Naegler <frank.naegler@typo3.org>
Thu, 20 Oct 2016 17:18:35 +0000 (19:18 +0200)
committerWouter Wolters <typo3@wouterwolters.nl>
Thu, 27 Oct 2016 19:36:51 +0000 (21:36 +0200)
This patch removes ExtDirect from EXT:workspaces and add a new AJAX disptacher.
The ExtDirect classes are renamend and moved into a new location.

Resolves: #78468
Releases: master
Change-Id: I7a60bfd1a790174d6013e16a0903b2ae2c244cc0
Reviewed-on: https://review.typo3.org/50294
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Daniel Lorenz <info@extco.de>
Tested-by: Daniel Lorenz <info@extco.de>
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters <typo3@wouterwolters.nl>
25 files changed:
typo3/sysext/backend/Resources/Public/JavaScript/ExtDirect.StateProvider.js
typo3/sysext/core/Documentation/Changelog/master/Breaking-78468-RemoveExtDirectFromEXTworkspaces.rst [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Controller/AjaxDispatcher.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Controller/PreviewController.php
typo3/sysext/workspaces/Classes/Controller/Remote/AbstractHandler.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Controller/Remote/ActionHandler.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Controller/Remote/MassActionHandler.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Controller/Remote/RemoteServer.php [new file with mode: 0644]
typo3/sysext/workspaces/Classes/Controller/ReviewController.php
typo3/sysext/workspaces/Classes/ExtDirect/AbstractHandler.php [deleted file]
typo3/sysext/workspaces/Classes/ExtDirect/ActionHandler.php [deleted file]
typo3/sysext/workspaces/Classes/ExtDirect/ExtDirectServer.php [deleted file]
typo3/sysext/workspaces/Classes/ExtDirect/MassActionHandler.php [deleted file]
typo3/sysext/workspaces/Classes/ExtDirect/PagetreeCollectionsProcessor.php [deleted file]
typo3/sysext/workspaces/Classes/Hooks/PagetreeCollectionsProcessor.php [new file with mode: 0644]
typo3/sysext/workspaces/Configuration/Backend/AjaxRoutes.php
typo3/sysext/workspaces/Resources/Public/JavaScript/Backend.js
typo3/sysext/workspaces/Resources/Public/JavaScript/Preview.js
typo3/sysext/workspaces/Resources/Public/JavaScript/Toolbar/WorkspacesMenu.js
typo3/sysext/workspaces/Resources/Public/JavaScript/Workspaces.js
typo3/sysext/workspaces/Tests/Functional/ActionHandler/ActionHandlerTest.php
typo3/sysext/workspaces/Tests/Unit/Controller/Remote/RemoteServerTest.php [new file with mode: 0644]
typo3/sysext/workspaces/Tests/Unit/ExtDirect/ExtDirectServerTest.php [deleted file]
typo3/sysext/workspaces/ext_localconf.php
typo3/sysext/workspaces/ext_tables.php

index 7d4aea3..978a7d1 100644 (file)
@@ -46,7 +46,7 @@ TYPO3.state.ExtDirectProvider = function(config) {
                 * @param {HttpProvider} this
                 */
                        'savefailure'
-                       );
+       );
 
                // call parent
        TYPO3.state.ExtDirectProvider.superclass.constructor.call(this);
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-78468-RemoveExtDirectFromEXTworkspaces.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-78468-RemoveExtDirectFromEXTworkspaces.rst
new file mode 100644 (file)
index 0000000..cce09bf
--- /dev/null
@@ -0,0 +1,45 @@
+.. include:: ../../Includes.txt
+
+=======================================================
+Breaking: #78468 - Remove ExtDirect from EXT:workspaces
+=======================================================
+
+See :issue:`78468`
+
+Description
+===========
+
+To remove ExtJS the ExtDirect component are removed too.
+A new class :php:`TYPO3\CMS\Workspaces\Controller\AjaxDispatcher` was added to implement the ExtDirect router functionality.
+This class is callable by a new AJAX route with the name `workspace_dispatch`.
+
+Impact
+======
+
+The following classes are moved:
+
+* EXT:workspaces/Classes/ExtDirect/AbstractHandler.php
+  => EXT:workspaces/Classes/Controller/Remote/AbstractHandler.php
+
+* EXT:workspaces/Classes/ExtDirect/ActionHandler.php
+  => EXT:workspaces/Classes/Controller/Remote/ActionHandler.php
+
+* EXT:workspaces/Classes/ExtDirect/MassActionHandler.php
+  => EXT:workspaces/Classes/Controller/Remote/MassActionHandler.php
+
+* EXT:workspaces/Classes/ExtDirect/ExtDirectServer.php
+  => EXT:workspaces/Classes/Controller/Remote/RemoteServer.php
+
+
+Affected Installations
+======================
+
+Any TYPO3 installation using the previously classes.
+
+
+Migration
+=========
+
+Use the new classes as mentioned above.
+
+.. index:: Backend, JavaScript, ext:workspaces
diff --git a/typo3/sysext/workspaces/Classes/Controller/AjaxDispatcher.php b/typo3/sysext/workspaces/Classes/Controller/AjaxDispatcher.php
new file mode 100644 (file)
index 0000000..2f16e04
--- /dev/null
@@ -0,0 +1,78 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Controller;
+
+/*
+ * 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 Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Workspaces\Controller\Remote\ActionHandler;
+use TYPO3\CMS\Workspaces\Controller\Remote\MassActionHandler;
+use TYPO3\CMS\Workspaces\Controller\Remote\RemoteServer;
+
+/**
+ * Implements the AJAX functionality for the various asynchronous calls
+ */
+class AjaxDispatcher
+{
+    /**
+     * @var array
+     */
+    protected $classMap = [
+        'RemoteServer' => RemoteServer::class,
+        'MassActions' => MassActionHandler::class,
+        'Actions' => ActionHandler::class
+    ];
+
+    /**
+     * @param ServerRequestInterface $request
+     * @param ResponseInterface $response
+     * @return ResponseInterface
+     */
+    public function dispatch(ServerRequestInterface $request, ResponseInterface $response)
+    {
+        $callStack = \GuzzleHttp\json_decode($request->getBody()->getContents());
+        if (!is_array($callStack)) {
+            $callStack = [$callStack];
+        }
+        $results = [];
+        foreach ($callStack as $call) {
+            $className = $this->classMap[$call->action];
+            $method = $call->method;
+            $parameters = $call->data;
+            $instance = GeneralUtility::makeInstance($className);
+            $results[] = $this->buildResultFromResponse(call_user_func_array([$instance, $method], $parameters), $call);
+        }
+        $response->getBody()->write(json_encode($results));
+        return $response;
+    }
+
+    /**
+     * @param mixed $responseFromMethod
+     * @param \stdClass $call
+     *
+     * @return \stdClass
+     */
+    protected function buildResultFromResponse($responseFromMethod, $call)
+    {
+        $tmp = new \stdClass();
+        $tmp->action = $call->action;
+        $tmp->debug = '';
+        $tmp->method = $call->method;
+        $tmp->result = $responseFromMethod;
+        $tmp->tid = $call->tid;
+        $tmp->type = $call->type;
+        return $tmp;
+    }
+}
index 9366731..7ac2d78 100644 (file)
@@ -16,7 +16,6 @@ namespace TYPO3\CMS\Workspaces\Controller;
 
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Backend\View\BackendTemplateView;
-use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Messaging\FlashMessage;
 use TYPO3\CMS\Core\Messaging\FlashMessageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -66,13 +65,6 @@ class PreviewController extends AbstractController
         parent::initializeAction();
         $this->stageService = GeneralUtility::makeInstance(StagesService::class);
         $this->workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
-        $this->pageRenderer->addJsFile('EXT:backend/Resources/Public/JavaScript/ExtDirect.StateProvider.js');
-        $this->pageRenderer->loadExtJS(false, false);
-        // Load  JavaScript:
-        $this->pageRenderer->addExtDirectCode([
-            'TYPO3.Workspaces',
-            'TYPO3.ExtDirectStateProvider'
-        ]);
         $states = $this->getBackendUser()->uc['moduleData']['Workspaces']['States'];
         $this->pageRenderer->addInlineSetting('Workspaces', 'States', $states);
         $this->pageRenderer->addInlineSetting('FormEngine', 'moduleUrl', BackendUtility::getModuleUrl('record_edit'));
@@ -150,7 +142,6 @@ class PreviewController extends AbstractController
 
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Workspaces/Preview');
         $this->pageRenderer->addInlineSetting('Workspaces', 'SplitPreviewModes', $splitPreviewModes);
-        $this->pageRenderer->addInlineSetting('Workspaces', 'token', FormProtectionFactory::get('backend')->generateToken('extDirect'));
 
         $cssFile = 'EXT:workspaces/Resources/Public/Css/preview.css';
         $cssFile = GeneralUtility::getFileAbsFileName($cssFile);
diff --git a/typo3/sysext/workspaces/Classes/Controller/Remote/AbstractHandler.php b/typo3/sysext/workspaces/Classes/Controller/Remote/AbstractHandler.php
new file mode 100644 (file)
index 0000000..9cad7ce
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Controller\Remote;
+
+/*
+ * 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!
+ */
+
+/**
+ * Class AbstractHandler
+ */
+abstract class AbstractHandler
+{
+    /**
+     * Gets the current workspace ID.
+     *
+     * @return int The current workspace ID
+     */
+    protected function getCurrentWorkspace()
+    {
+        return $this->getWorkspaceService()->getCurrentWorkspace();
+    }
+
+    /**
+     * Gets an error response to be shown in the grid component.
+     *
+     * @param string $errorLabel Name of the label in the locallang.xlf file
+     * @param int $errorCode The error code to be used
+     * @param bool $successFlagValue Value of the success flag to be delivered back (might be FALSE in most cases)
+     * @return array
+     */
+    protected function getErrorResponse($errorLabel, $errorCode = 0, $successFlagValue = false)
+    {
+        $localLangFile = 'LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf';
+        $response = [
+            'error' => [
+                'code' => $errorCode,
+                'message' => $GLOBALS['LANG']->sL($localLangFile . ':' . $errorLabel)
+            ],
+            'success' => $successFlagValue
+        ];
+        return $response;
+    }
+
+    /**
+     * Gets an instance of the workspaces service.
+     *
+     * @return \TYPO3\CMS\Workspaces\Service\WorkspaceService
+     */
+    protected function getWorkspaceService()
+    {
+        return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class);
+    }
+
+    /**
+     * Validates whether the submitted language parameter can be
+     * interpreted as integer value.
+     *
+     * @param stdClass $parameters
+     * @return int|NULL
+     */
+    protected function validateLanguageParameter(\stdClass $parameters)
+    {
+        $language = null;
+        if (isset($parameters->language) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($parameters->language)) {
+            $language = $parameters->language;
+        }
+        return $language;
+    }
+
+    /**
+     * Gets affected elements on publishing/swapping actions.
+     * Affected elements have a dependency, e.g. translation overlay
+     * and the default origin record - thus, the default record would be
+     * affected if the translation overlay shall be published.
+     *
+     * @param stdClass $parameters
+     * @return array
+     */
+    protected function getAffectedElements(\stdClass $parameters)
+    {
+        $affectedElements = [];
+        if ($parameters->type === 'selection') {
+            foreach ((array)$parameters->selection as $element) {
+                $affectedElements[] = \TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord::create($element->table, $element->liveId, $element->versionId);
+            }
+        } elseif ($parameters->type === 'all') {
+            $versions = $this->getWorkspaceService()->selectVersionsInWorkspace($this->getCurrentWorkspace(), 0, -99, -1, 0, 'tables_select', $this->validateLanguageParameter($parameters));
+            foreach ($versions as $table => $tableElements) {
+                foreach ($tableElements as $element) {
+                    $affectedElement = \TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord::create($table, $element['t3ver_oid'], $element['uid']);
+                    $affectedElement->getVersionRecord()->setRow($element);
+                    $affectedElements[] = $affectedElement;
+                }
+            }
+        }
+        return $affectedElements;
+    }
+
+    /**
+     * Creates a new instance of the integrity service for the
+     * given set of affected elements.
+     *
+     * @param \TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord[] $affectedElements
+     * @return \TYPO3\CMS\Workspaces\Service\IntegrityService
+     * @see getAffectedElements
+     */
+    protected function createIntegrityService(array $affectedElements)
+    {
+        /** @var $integrityService \TYPO3\CMS\Workspaces\Service\IntegrityService */
+        $integrityService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\IntegrityService::class);
+        $integrityService->setAffectedElements($affectedElements);
+        return $integrityService;
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Controller/Remote/ActionHandler.php b/typo3/sysext/workspaces/Classes/Controller/Remote/ActionHandler.php
new file mode 100644 (file)
index 0000000..8484849
--- /dev/null
@@ -0,0 +1,854 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Controller\Remote;
+
+/*
+ * 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\ExtensionManagementUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Fluid\View\StandaloneView;
+use TYPO3\CMS\Workspaces\Domain\Record\StageRecord;
+use TYPO3\CMS\Workspaces\Domain\Record\WorkspaceRecord;
+use TYPO3\CMS\Workspaces\Service\StagesService;
+use TYPO3\CMS\Workspaces\Service\WorkspaceService;
+
+/**
+ * Class ActionHandler
+ */
+class ActionHandler extends AbstractHandler
+{
+    /**
+     * @var StagesService
+     */
+    protected $stageService;
+
+    /**
+     * Creates this object.
+     */
+    public function __construct()
+    {
+        $this->stageService = GeneralUtility::makeInstance(StagesService::class);
+    }
+
+    /**
+     * Generates a workspace preview link.
+     *
+     * @param int $uid The ID of the record to be linked
+     * @return string the full domain including the protocol http:// or https://, but without the trailing '/'
+     */
+    public function generateWorkspacePreviewLink($uid)
+    {
+        return $this->getWorkspaceService()->generateWorkspacePreviewLink($uid);
+    }
+
+    /**
+     * Generates workspace preview links for all available languages of a page.
+     *
+     * @param int $uid
+     * @return array
+     */
+    public function generateWorkspacePreviewLinksForAllLanguages($uid)
+    {
+        return $this->getWorkspaceService()->generateWorkspacePreviewLinksForAllLanguages($uid);
+    }
+
+    /**
+     * Swaps a single record.
+     *
+     * @param string $table
+     * @param int $t3ver_oid
+     * @param int $orig_uid
+     * @return void
+     * @todo What about reporting errors back to the ExtJS interface? /olly/
+     */
+    public function swapSingleRecord($table, $t3ver_oid, $orig_uid)
+    {
+        $versionRecord = BackendUtility::getRecord($table, $orig_uid);
+        $currentWorkspace = $this->setTemporaryWorkspace($versionRecord['t3ver_wsid']);
+
+        $cmd[$table][$t3ver_oid]['version'] = [
+            'action' => 'swap',
+            'swapWith' => $orig_uid,
+            'swapIntoWS' => 1
+        ];
+        $this->processTcaCmd($cmd);
+
+        $this->setTemporaryWorkspace($currentWorkspace);
+    }
+
+    /**
+     * Deletes a single record.
+     *
+     * @param string $table
+     * @param int $uid
+     * @return void
+     * @todo What about reporting errors back to the ExtJS interface? /olly/
+     */
+    public function deleteSingleRecord($table, $uid)
+    {
+        $versionRecord = BackendUtility::getRecord($table, $uid);
+        $currentWorkspace = $this->setTemporaryWorkspace($versionRecord['t3ver_wsid']);
+
+        $cmd[$table][$uid]['version'] = [
+            'action' => 'clearWSID'
+        ];
+        $this->processTcaCmd($cmd);
+
+        $this->setTemporaryWorkspace($currentWorkspace);
+    }
+
+    /**
+     * Generates a view link for a page.
+     *
+     * @param string $table
+     * @param string $uid
+     * @return string
+     */
+    public function viewSingleRecord($table, $uid)
+    {
+        return WorkspaceService::viewSingleRecord($table, $uid);
+    }
+
+    /**
+     * Executes an action (publish, discard, swap) to a selection set.
+     *
+     * @param \stdClass $parameter
+     * @return array
+     */
+    public function executeSelectionAction($parameter)
+    {
+        $result = [];
+
+        if (empty($parameter->action) || empty($parameter->selection)) {
+            $result['error'] = 'No action or record selection given';
+            return $result;
+        }
+
+        $commands = [];
+        $swapIntoWorkspace = ($parameter->action === 'swap');
+        if ($parameter->action === 'publish' || $swapIntoWorkspace) {
+            $commands = $this->getPublishSwapCommands($parameter->selection, $swapIntoWorkspace);
+        } elseif ($parameter->action === 'discard') {
+            $commands = $this->getFlushCommands($parameter->selection);
+        }
+
+        $result = $this->processTcaCmd($commands);
+        $result['total'] = count($commands);
+        return $result;
+    }
+
+    /**
+     * Get publish swap commands
+     *
+     * @param array|\stdClass[] $selection
+     * @param bool $swapIntoWorkspace
+     * @return array
+     */
+    protected function getPublishSwapCommands(array $selection, $swapIntoWorkspace)
+    {
+        $commands = [];
+        foreach ($selection as $record) {
+            $commands[$record->table][$record->liveId]['version'] = [
+                'action' => 'swap',
+                'swapWith' => $record->versionId,
+                'swapIntoWS' => (bool)$swapIntoWorkspace,
+            ];
+        }
+        return $commands;
+    }
+
+    /**
+     * Get flush commands
+     *
+     * @param array|\stdClass[] $selection
+     * @return array
+     */
+    protected function getFlushCommands(array $selection)
+    {
+        $commands = [];
+        foreach ($selection as $record) {
+            $commands[$record->table][$record->versionId]['version'] = [
+                'action' => 'clearWSID',
+            ];
+        }
+        return $commands;
+    }
+
+    /**
+     * Saves the selected columns to be shown to the preferences of the current backend user.
+     *
+     * @param \stdClass $model
+     * @return void
+     */
+    public function saveColumnModel($model)
+    {
+        $data = [];
+        foreach ($model as $column) {
+            $data[$column->column] = [
+                'position' => $column->position,
+                'hidden' => $column->hidden
+            ];
+        }
+        $GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['columns'] = $data;
+        $GLOBALS['BE_USER']->writeUC();
+    }
+
+    public function loadColumnModel()
+    {
+        if (is_array($GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['columns'])) {
+            return $GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['columns'];
+        } else {
+            return [];
+        }
+    }
+
+    /**
+     * Saves the selected language.
+     *
+     * @param int|string $language
+     * @return void
+     */
+    public function saveLanguageSelection($language)
+    {
+        if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($language) === false && $language !== 'all') {
+            $language = 'all';
+        }
+        $GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['language'] = $language;
+        $GLOBALS['BE_USER']->writeUC();
+    }
+
+    /**
+     * Gets the dialog window to be displayed before a record can be sent to the next stage.
+     *
+     * @param int $uid
+     * @param string $table
+     * @param int $t3ver_oid
+     * @return array
+     */
+    public function sendToNextStageWindow($uid, $table, $t3ver_oid)
+    {
+        $elementRecord = BackendUtility::getRecord($table, $uid);
+        $currentWorkspace = $this->setTemporaryWorkspace($elementRecord['t3ver_wsid']);
+
+        if (is_array($elementRecord)) {
+            $workspaceRecord = WorkspaceRecord::get($elementRecord['t3ver_wsid']);
+            $nextStageRecord = $workspaceRecord->getNextStage($elementRecord['t3ver_stage']);
+            if ($nextStageRecord !== null) {
+                $this->stageService->getRecordService()->add($table, $uid);
+                $result = $this->getSentToStageWindow($nextStageRecord);
+                $result['affects'] = [
+                    'table' => $table,
+                    'nextStage' => $nextStageRecord->getUid(),
+                    't3ver_oid' => $t3ver_oid,
+                    'uid' => $uid
+                ];
+            } else {
+                $result = $this->getErrorResponse('error.stageId.invalid', 1291111644);
+            }
+        } else {
+            $result = $this->getErrorResponse('error.sendToNextStage.noRecordFound', 1287264776);
+        }
+
+        $this->setTemporaryWorkspace($currentWorkspace);
+        return $result;
+    }
+
+    /**
+     * Gets the dialog window to be displayed before a record can be sent to the previous stage.
+     *
+     * @param int $uid
+     * @param string $table
+     * @return array
+     */
+    public function sendToPrevStageWindow($uid, $table)
+    {
+        $elementRecord = BackendUtility::getRecord($table, $uid);
+        $currentWorkspace = $this->setTemporaryWorkspace($elementRecord['t3ver_wsid']);
+
+        if (is_array($elementRecord)) {
+            $workspaceRecord = WorkspaceRecord::get($elementRecord['t3ver_wsid']);
+            $stageRecord = $workspaceRecord->getStage($elementRecord['t3ver_stage']);
+
+            if ($stageRecord !== null) {
+                if (!$stageRecord->isEditStage()) {
+                    $this->stageService->getRecordService()->add($table, $uid);
+                    $previousStageRecord = $stageRecord->getPrevious();
+                    $result = $this->getSentToStageWindow($previousStageRecord);
+                    $result['affects'] = [
+                        'table' => $table,
+                        'uid' => $uid,
+                        'nextStage' => $previousStageRecord->getUid()
+                    ];
+                } else {
+                    // element is already in edit stage, there is no prev stage - return an error message
+                    $result = $this->getErrorResponse('error.sendToPrevStage.noPreviousStage', 1287264746);
+                }
+            } else {
+                $result = $this->getErrorResponse('error.stageId.invalid', 1291111644);
+            }
+        } else {
+            $result = $this->getErrorResponse('error.sendToNextStage.noRecordFound', 1287264765);
+        }
+
+        $this->setTemporaryWorkspace($currentWorkspace);
+        return $result;
+    }
+
+    /**
+     * Gets the dialog window to be displayed before a record can be sent to a specific stage.
+     *
+     * @param int $nextStageId
+     * @param array|\stdClass[] $elements
+     * @return array
+     */
+    public function sendToSpecificStageWindow($nextStageId, array $elements)
+    {
+        foreach ($elements as $element) {
+            $this->stageService->getRecordService()->add(
+                $element->table,
+                $element->uid
+            );
+        }
+
+        $result = $this->getSentToStageWindow($nextStageId);
+        $result['affects'] = [
+            'nextStage' => $nextStageId
+        ];
+        return $result;
+    }
+
+    /**
+     * Gets a merged variant of recipient defined by uid and custom ones.
+     *
+     * @param array $uidOfRecipients list of recipients
+     * @param string $additionalRecipients given user string of additional recipients
+     * @param int $stageId stage id
+     * @return array
+     * @throws \InvalidArgumentException
+     */
+    public function getRecipientList(array $uidOfRecipients, $additionalRecipients, $stageId)
+    {
+        $stageRecord = WorkspaceRecord::get($this->getCurrentWorkspace())->getStage($stageId);
+
+        if ($stageRecord === null) {
+            throw new \InvalidArgumentException(
+                $GLOBALS['LANG']->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.stageId.integer'),
+                1476044776
+            );
+        }
+
+        $recipients = [];
+        $finalRecipients = [];
+        $backendUserIds = $stageRecord->getAllRecipients();
+        foreach ($uidOfRecipients as $userUid) {
+            // Ensure that only configured backend users are considered
+            if (!in_array($userUid, $backendUserIds)) {
+                continue;
+            }
+            $beUserRecord = BackendUtility::getRecord('be_users', (int)$userUid);
+            if (is_array($beUserRecord) && $beUserRecord['email'] !== '') {
+                $uc = $beUserRecord['uc'] ? unserialize($beUserRecord['uc']) : [];
+                $recipients[$beUserRecord['email']] = [
+                    'email' => $beUserRecord['email'],
+                    'lang' => isset($uc['lang']) ? $uc['lang'] : $beUserRecord['lang']
+                ];
+            }
+        }
+
+        if ($stageRecord->hasPreselection() && !$stageRecord->isPreselectionChangeable()) {
+            $preselectedBackendUsers = $this->getStageService()->getBackendUsers(
+                implode(',', $this->stageService->getPreselectedRecipients($stageRecord))
+            );
+
+            foreach ($preselectedBackendUsers as $preselectedBackendUser) {
+                if (empty($preselectedBackendUser['email']) || !GeneralUtility::validEmail($preselectedBackendUser['email'])) {
+                    continue;
+                }
+                if (!isset($recipients[$preselectedBackendUser['email']])) {
+                    $uc = (!empty($preselectedBackendUser['uc']) ? unserialize($preselectedBackendUser['uc']) : []);
+                    $recipients[$preselectedBackendUser['email']] = [
+                        'email' => $preselectedBackendUser['email'],
+                        'lang' => (isset($uc['lang']) ? $uc['lang'] : $preselectedBackendUser['lang'])
+                    ];
+                }
+            }
+        }
+
+        if ($additionalRecipients !== '') {
+            $emails = GeneralUtility::trimExplode(LF, $additionalRecipients, true);
+            $additionalRecipients = [];
+            foreach ($emails as $email) {
+                $additionalRecipients[$email] = ['email' => $email];
+            }
+        } else {
+            $additionalRecipients = [];
+        }
+        // We merge $recipients on top of $additionalRecipients because $recipients
+        // possibly is more complete with a user language. Furthermore, the list of
+        // recipients is automatically unique since we indexed $additionalRecipients
+        // and $recipients with the email address
+        $allRecipients = array_merge($additionalRecipients, $recipients);
+        foreach ($allRecipients as $email => $recipientInformation) {
+            if (GeneralUtility::validEmail($email)) {
+                $finalRecipients[] = $recipientInformation;
+            }
+        }
+        return $finalRecipients;
+    }
+
+    /**
+     * Discard all items from given page id.
+     *
+     * @param int $pageId
+     * @return array
+     */
+    public function discardStagesFromPage($pageId)
+    {
+        $cmdMapArray = [];
+        /** @var $workspaceService WorkspaceService */
+        $workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
+        /** @var $stageService StagesService */
+        $stageService = GeneralUtility::makeInstance(StagesService::class);
+        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($stageService->getWorkspaceId(), ($filter = 1), ($stage = -99), $pageId, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
+        foreach ($workspaceItemsArray as $tableName => $items) {
+            foreach ($items as $item) {
+                $cmdMapArray[$tableName][$item['uid']]['version']['action'] = 'clearWSID';
+            }
+        }
+        $this->processTcaCmd($cmdMapArray);
+        return [
+            'success' => true
+        ];
+    }
+
+    /**
+     * Push the given element collection to the next workspace stage.
+     *
+     * <code>
+     * $parameters->additional = your@mail.com
+     * $parameters->affects->__TABLENAME__
+     * $parameters->comments
+     * $parameters->receipients
+     * $parameters->stageId
+     * </code>
+     *
+     * @param \stdClass $parameters
+     * @return array
+     */
+    public function sentCollectionToStage(\stdClass $parameters)
+    {
+        $cmdMapArray = [];
+        $comment = $parameters->comments;
+        $stageId = $parameters->stageId;
+        if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($stageId) === false) {
+            throw new \InvalidArgumentException('Missing "stageId" in $parameters array.', 1319488194);
+        }
+        if (!is_object($parameters->affects) || empty($parameters->affects)) {
+            throw new \InvalidArgumentException('Missing "affected items" in $parameters array.', 1319488195);
+        }
+        $recipients = $this->getRecipientList((array)$parameters->recipients, $parameters->additional, $stageId);
+        foreach ($parameters->affects as $tableName => $items) {
+            foreach ($items as $item) {
+                // Publishing uses live id in command map
+                if ($stageId == StagesService::STAGE_PUBLISH_EXECUTE_ID) {
+                    $cmdMapArray[$tableName][$item->t3ver_oid]['version']['action'] = 'swap';
+                    $cmdMapArray[$tableName][$item->t3ver_oid]['version']['swapWith'] = $item->uid;
+                    $cmdMapArray[$tableName][$item->t3ver_oid]['version']['comment'] = $comment;
+                    $cmdMapArray[$tableName][$item->t3ver_oid]['version']['notificationAlternativeRecipients'] = $recipients;
+                // Setting stage uses version id in command map
+                } else {
+                    $cmdMapArray[$tableName][$item->uid]['version']['action'] = 'setStage';
+                    $cmdMapArray[$tableName][$item->uid]['version']['stageId'] = $stageId;
+                    $cmdMapArray[$tableName][$item->uid]['version']['comment'] = $comment;
+                    $cmdMapArray[$tableName][$item->uid]['version']['notificationAlternativeRecipients'] = $recipients;
+                }
+            }
+        }
+        $this->processTcaCmd($cmdMapArray);
+        return [
+            'success' => true,
+            // force refresh after publishing changes
+            'refreshLivePanel' => $parameters->stageId == -20
+        ];
+    }
+
+    /**
+     * Process TCA command map array.
+     *
+     * @param array $cmdMapArray
+     * @return array
+     */
+    protected function processTcaCmd(array $cmdMapArray)
+    {
+        $result = [];
+
+        if (empty($cmdMapArray)) {
+            $result['error'] = 'No commands given to be processed';
+            return $result;
+        }
+
+        /** @var \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler */
+        $dataHandler = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
+        $dataHandler->start([], $cmdMapArray);
+        $dataHandler->process_cmdmap();
+
+        if ($dataHandler->errorLog) {
+            $result['error'] = implode('<br/>', $dataHandler->errorLog);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Gets an object with this structure:
+     *
+     * affects: object
+     * table
+     * t3ver_oid
+     * nextStage
+     * uid
+     * receipients: array with uids
+     * additional: string
+     * comments: string
+     *
+     * @param \stdClass $parameters
+     * @return array
+     */
+    public function sendToNextStageExecute(\stdClass $parameters)
+    {
+        $cmdArray = [];
+        $setStageId = $parameters->affects->nextStage;
+        $comments = $parameters->comments;
+        $table = $parameters->affects->table;
+        $uid = $parameters->affects->uid;
+        $t3ver_oid = $parameters->affects->t3ver_oid;
+
+        $elementRecord = BackendUtility::getRecord($table, $uid);
+        $currentWorkspace = $this->setTemporaryWorkspace($elementRecord['t3ver_wsid']);
+
+        $recipients = $this->getRecipientList((array)$parameters->recipients, $parameters->additional, $setStageId);
+        if ($setStageId == StagesService::STAGE_PUBLISH_EXECUTE_ID) {
+            $cmdArray[$table][$t3ver_oid]['version']['action'] = 'swap';
+            $cmdArray[$table][$t3ver_oid]['version']['swapWith'] = $uid;
+            $cmdArray[$table][$t3ver_oid]['version']['comment'] = $comments;
+            $cmdArray[$table][$t3ver_oid]['version']['notificationAlternativeRecipients'] = $recipients;
+        } else {
+            $cmdArray[$table][$uid]['version']['action'] = 'setStage';
+            $cmdArray[$table][$uid]['version']['stageId'] = $setStageId;
+            $cmdArray[$table][$uid]['version']['comment'] = $comments;
+            $cmdArray[$table][$uid]['version']['notificationAlternativeRecipients'] = $recipients;
+        }
+        $this->processTcaCmd($cmdArray);
+        $result = [
+            'success' => true
+        ];
+
+        $this->setTemporaryWorkspace($currentWorkspace);
+        return $result;
+    }
+
+    /**
+     * Gets an object with this structure:
+     *
+     * affects: object
+     * table
+     * t3ver_oid
+     * nextStage
+     * receipients: array with uids
+     * additional: string
+     * comments: string
+     *
+     * @param \stdClass $parameters
+     * @return array
+     */
+    public function sendToPrevStageExecute(\stdClass $parameters)
+    {
+        $cmdArray = [];
+        $setStageId = $parameters->affects->nextStage;
+        $comments = $parameters->comments;
+        $table = $parameters->affects->table;
+        $uid = $parameters->affects->uid;
+
+        $elementRecord = BackendUtility::getRecord($table, $uid);
+        $currentWorkspace = $this->setTemporaryWorkspace($elementRecord['t3ver_wsid']);
+
+        $recipients = $this->getRecipientList((array)$parameters->recipients, $parameters->additional, $setStageId);
+        $cmdArray[$table][$uid]['version']['action'] = 'setStage';
+        $cmdArray[$table][$uid]['version']['stageId'] = $setStageId;
+        $cmdArray[$table][$uid]['version']['comment'] = $comments;
+        $cmdArray[$table][$uid]['version']['notificationAlternativeRecipients'] = $recipients;
+        $this->processTcaCmd($cmdArray);
+        $result = [
+            'success' => true
+        ];
+
+        $this->setTemporaryWorkspace($currentWorkspace);
+        return $result;
+    }
+
+    /**
+     * Gets an object with this structure:
+     *
+     * affects: object
+     * elements: array
+     * 0: object
+     * table
+     * t3ver_oid
+     * uid
+     * 1: object
+     * table
+     * t3ver_oid
+     * uid
+     * nextStage
+     * recipients: array with uids
+     * additional: string
+     * comments: string
+     *
+     * @param \stdClass $parameters
+     * @return array
+     */
+    public function sendToSpecificStageExecute(\stdClass $parameters)
+    {
+        $cmdArray = [];
+        $setStageId = $parameters->affects->nextStage;
+        $comments = $parameters->comments;
+        $elements = $parameters->affects->elements;
+        $recipients = $this->getRecipientList((array)$parameters->recipients, $parameters->additional, $setStageId);
+        foreach ($elements as $element) {
+            // Avoid any action on records that have already been published to live
+            $elementRecord = BackendUtility::getRecord($element->table, $element->uid);
+            if ((int)$elementRecord['t3ver_wsid'] === 0) {
+                continue;
+            }
+
+            if ($setStageId == StagesService::STAGE_PUBLISH_EXECUTE_ID) {
+                $cmdArray[$element->table][$element->t3ver_oid]['version']['action'] = 'swap';
+                $cmdArray[$element->table][$element->t3ver_oid]['version']['swapWith'] = $element->uid;
+                $cmdArray[$element->table][$element->t3ver_oid]['version']['comment'] = $comments;
+                $cmdArray[$element->table][$element->t3ver_oid]['version']['notificationAlternativeRecipients'] = $recipients;
+            } else {
+                $cmdArray[$element->table][$element->uid]['version']['action'] = 'setStage';
+                $cmdArray[$element->table][$element->uid]['version']['stageId'] = $setStageId;
+                $cmdArray[$element->table][$element->uid]['version']['comment'] = $comments;
+                $cmdArray[$element->table][$element->uid]['version']['notificationAlternativeRecipients'] = $recipients;
+            }
+        }
+        $this->processTcaCmd($cmdArray);
+        $result = [
+            'success' => true
+        ];
+        return $result;
+    }
+
+    /**
+     * Gets the dialog window to be displayed before a record can be sent to a stage.
+     *
+     * @param StageRecord|int $nextStage
+     * @return array
+     */
+    protected function getSentToStageWindow($nextStage)
+    {
+        if (!$nextStage instanceof StageRecord) {
+            $nextStage = WorkspaceRecord::get($this->getCurrentWorkspace())->getStage($nextStage);
+        }
+
+        $result = [];
+        if ($nextStage->isDialogEnabled()) {
+            $result['sendMailTo'] = $this->getRecipientsOfStage($nextStage->getUid());
+            $result['additional'] = [
+                'type' => 'textarea',
+                'value' => ''
+            ];
+        }
+        $result['comments'] = [
+            'type' => 'textarea',
+            'value' => ($nextStage->isInternal() ? '' : $nextStage->getDefaultComment())
+        ];
+
+        return $result;
+    }
+
+    /**
+     * Gets all assigned recipients of a particular stage.
+     *
+     * @param StageRecord|int $stageRecord
+     * @return array
+     */
+    protected function getRecipientsOfStage($stageRecord)
+    {
+        if (!$stageRecord instanceof StageRecord) {
+            $stageRecord = WorkspaceRecord::get($this->getCurrentWorkspace())->getStage($stageRecord);
+        }
+
+        $result = [];
+        $allRecipients = $this->getStageService()->getResponsibleBeUser($stageRecord);
+        $preselectedRecipients = $this->stageService->getPreselectedRecipients($stageRecord);
+        $isPreselectionChangeable = $stageRecord->isPreselectionChangeable();
+
+        foreach ($allRecipients as $backendUserId => $backendUser) {
+            if (empty($backendUser['email']) || !GeneralUtility::validEmail($backendUser['email'])) {
+                continue;
+            }
+
+            $name = (!empty($backendUser['realName']) ? $backendUser['realName'] : $backendUser['username']);
+            $checked = in_array($backendUserId, $preselectedRecipients);
+            $disabled = ($checked && !$isPreselectionChangeable);
+
+            $result[] = [
+                'label' => sprintf('%s (%s)', $name, $backendUser['email']),
+                'value' => $backendUserId,
+                'name' => 'recipients-' . $backendUserId,
+                'checked' => $checked,
+                'disabled' => $disabled
+            ];
+        }
+
+        return $result;
+    }
+
+    /**
+     * Gets the default comment of a particular stage.
+     *
+     * @param int $stage
+     * @return string
+     */
+    protected function getDefaultCommentOfStage($stage)
+    {
+        $result = $this->getStageService()->getPropertyOfCurrentWorkspaceStage($stage, 'default_mailcomment');
+        return $result;
+    }
+
+    /**
+     * Gets an instance of the Stage service.
+     *
+     * @return StagesService
+     */
+    protected function getStageService()
+    {
+        if (!isset($this->stageService)) {
+            $this->stageService = GeneralUtility::makeInstance(StagesService::class);
+        }
+        return $this->stageService;
+    }
+
+    /**
+     * Send all available workspace records to the previous stage.
+     *
+     * @param int $id Current page id to process items to previous stage.
+     * @return array
+     */
+    public function sendPageToPreviousStage($id)
+    {
+        $workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
+        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($this->stageService->getWorkspaceId(), ($filter = 1), ($stage = -99), $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
+        list($currentStage, $previousStage) = $this->getStageService()->getPreviousStageForElementCollection($workspaceItemsArray);
+        // get only the relevant items for processing
+        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($this->stageService->getWorkspaceId(), ($filter = 1), $currentStage['uid'], $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
+        $stageFormFields = $this->getSentToStageWindow($previousStage['uid']);
+        $result = array_merge($stageFormFields, [
+            'title' => 'Status message: Page send to next stage - ID: ' . $id . ' - Next stage title: ' . $previousStage['title'],
+            'items' => $this->getSentToStageWindow($previousStage['uid']),
+            'affects' => $workspaceItemsArray,
+            'stageId' => $previousStage['uid']
+        ]);
+        return $result;
+    }
+
+    /**
+     * @param int $id Current Page id to select Workspace items from.
+     * @return array
+     */
+    public function sendPageToNextStage($id)
+    {
+        $workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
+        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($this->stageService->getWorkspaceId(), ($filter = 1), ($stage = -99), $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
+        list($currentStage, $nextStage) = $this->getStageService()->getNextStageForElementCollection($workspaceItemsArray);
+        // get only the relevant items for processing
+        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($this->stageService->getWorkspaceId(), ($filter = 1), $currentStage['uid'], $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
+        $stageFormFields = $this->getSentToStageWindow($nextStage['uid']);
+        $result = array_merge($stageFormFields, [
+            'title' => 'Status message: Page send to next stage - ID: ' . $id . ' - Next stage title: ' . $nextStage['title'],
+            'affects' => $workspaceItemsArray,
+            'stageId' => $nextStage['uid']
+        ]);
+        return $result;
+    }
+
+    /**
+     * Fetch the current label and visible state of the buttons.
+     *
+     * @param int $id
+     * @return string The pre-rendered HTML for the stage buttons
+     */
+    public function updateStageChangeButtons($id)
+    {
+        /** @var StagesService $stageService */
+        $stageService = GeneralUtility::makeInstance(StagesService::class);
+        /** @var WorkspaceService $workspaceService */
+        $workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
+        // fetch the next and previous stage
+        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($stageService->getWorkspaceId(), ($filter = 1), ($stage = -99), $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
+        list(, $nextStage) = $stageService->getNextStageForElementCollection($workspaceItemsArray);
+        list(, $previousStage) = $stageService->getPreviousStageForElementCollection($workspaceItemsArray);
+
+        /** @var StandaloneView $view */
+        $view = GeneralUtility::makeInstance(StandaloneView::class);
+        $extensionPath = ExtensionManagementUtility::extPath('workspaces');
+        $view->setPartialRootPaths(['default' => $extensionPath . 'Resources/Private/Partials']);
+        $view->setTemplatePathAndFilename($extensionPath . 'Resources/Private/Templates/Preview/Ajax/StageButtons.html');
+        $request = $view->getRequest();
+        $request->setControllerExtensionName('workspaces');
+        $view->assignMultiple([
+            'enablePreviousStageButton' => is_array($previousStage) && !empty($previousStage),
+            'enableNextStageButton' => is_array($nextStage) && !empty($nextStage),
+            'enableDiscardStageButton' => is_array($nextStage) && !empty($nextStage) || is_array($previousStage) && !empty($previousStage),
+            'nextStage' => $nextStage['title'],
+            'nextStageId' => $nextStage['uid'],
+            'prevStage' => $previousStage['title'],
+            'prevStageId' => $previousStage['uid'],
+        ]);
+        $renderedView = $view->render();
+        return $renderedView;
+    }
+
+    /**
+     * @param int $workspaceId
+     * @return int Id of the original workspace
+     * @throws \TYPO3\CMS\Core\Exception
+     */
+    protected function setTemporaryWorkspace($workspaceId)
+    {
+        $workspaceId = (int)$workspaceId;
+        $currentWorkspace = (int)$this->getBackendUser()->workspace;
+
+        if ($currentWorkspace !== $workspaceId) {
+            if (!$this->getBackendUser()->setTemporaryWorkspace($workspaceId)) {
+                throw new \TYPO3\CMS\Core\Exception(
+                    'Cannot set temporary workspace to "' . $workspaceId . '"',
+                    1371484524
+                );
+            }
+        }
+
+        return $currentWorkspace;
+    }
+
+    /**
+     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
+     */
+    protected function getBackendUser()
+    {
+        return $GLOBALS['BE_USER'];
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Controller/Remote/MassActionHandler.php b/typo3/sysext/workspaces/Classes/Controller/Remote/MassActionHandler.php
new file mode 100644 (file)
index 0000000..d3d0c9b
--- /dev/null
@@ -0,0 +1,218 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Controller\Remote;
+
+/*
+ * 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!
+ */
+
+/**
+ * Class MassActionHandler
+ * Class encapsulates all actions which are triggered for all elements within the current workspace.
+ */
+class MassActionHandler extends AbstractHandler
+{
+    const MAX_RECORDS_TO_PROCESS = 30;
+
+    /**
+     * Path to the locallang file
+     *
+     * @var string
+     */
+    private $pathToLocallang = 'LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf';
+
+    /**
+     * Get list of available mass workspace actions.
+     *
+     * @param \stdClass $parameter
+     * @return array $data
+     */
+    public function getMassStageActions($parameter)
+    {
+        $actions = [];
+        $currentWorkspace = $this->getCurrentWorkspace();
+        $massActionsEnabled = $GLOBALS['BE_USER']->getTSConfigVal('options.workspaces.enableMassActions') !== '0';
+        // in case we're working within "All Workspaces" we can't provide Mass Actions
+        if ($currentWorkspace != \TYPO3\CMS\Workspaces\Service\WorkspaceService::SELECT_ALL_WORKSPACES && $massActionsEnabled) {
+            $publishAccess = $GLOBALS['BE_USER']->workspacePublishAccess($currentWorkspace);
+            if ($publishAccess && !($GLOBALS['BE_USER']->workspaceRec['publish_access'] & 1)) {
+                $actions[] = ['action' => 'publish', 'title' => $GLOBALS['LANG']->sL($this->pathToLocallang . ':label_doaction_publish')];
+                if ($GLOBALS['BE_USER']->workspaceSwapAccess()) {
+                    $actions[] = ['action' => 'swap', 'title' => $GLOBALS['LANG']->sL($this->pathToLocallang . ':label_doaction_swap')];
+                }
+            }
+            if ($currentWorkspace !== \TYPO3\CMS\Workspaces\Service\WorkspaceService::LIVE_WORKSPACE_ID) {
+                $actions[] = ['action' => 'discard', 'title' => $GLOBALS['LANG']->sL($this->pathToLocallang . ':label_doaction_discard')];
+            }
+        }
+        $result = [
+            'total' => count($actions),
+            'data' => $actions
+        ];
+        return $result;
+    }
+
+    /**
+     * Publishes the current workspace.
+     *
+     * @param \stdClass $parameters
+     * @return array
+     */
+    public function publishWorkspace(\stdClass $parameters)
+    {
+        $result = [
+            'init' => false,
+            'total' => 0,
+            'processed' => 0,
+            'error' => false
+        ];
+        try {
+            if ($parameters->init) {
+                $language = $this->validateLanguageParameter($parameters);
+                $cnt = $this->initPublishData($this->getCurrentWorkspace(), $parameters->swap, $language);
+                $result['total'] = $cnt;
+            } else {
+                $result['processed'] = $this->processData($this->getCurrentWorkspace());
+                $result['total'] = $GLOBALS['BE_USER']->getSessionData('workspaceMassAction_total');
+            }
+        } catch (\Exception $e) {
+            $result['error'] = $e->getMessage();
+        }
+        return $result;
+    }
+
+    /**
+     * Flushes the current workspace.
+     *
+     * @param \stdClass $parameters
+     * @return array
+     */
+    public function flushWorkspace(\stdClass $parameters)
+    {
+        $result = [
+            'init' => false,
+            'total' => 0,
+            'processed' => 0,
+            'error' => false
+        ];
+        try {
+            if ($parameters->init) {
+                $language = $this->validateLanguageParameter($parameters);
+                $cnt = $this->initFlushData($this->getCurrentWorkspace(), $language);
+                $result['total'] = $cnt;
+            } else {
+                $result['processed'] = $this->processData($this->getCurrentWorkspace());
+                $result['total'] = $GLOBALS['BE_USER']->getSessionData('workspaceMassAction_total');
+            }
+        } catch (\Exception $e) {
+            $result['error'] = $e->getMessage();
+        }
+        return $result;
+    }
+
+    /**
+     * Initializes the command map to be used for publishing.
+     *
+     * @param int $workspace
+     * @param bool $swap
+     * @param int $language
+     * @return int
+     */
+    protected function initPublishData($workspace, $swap, $language = null)
+    {
+        // workspace might be -98 a.k.a "All Workspaces but that's save here
+        $publishData = $this->getWorkspaceService()->getCmdArrayForPublishWS($workspace, $swap, 0, $language);
+        $recordCount = 0;
+        foreach ($publishData as $table => $recs) {
+            $recordCount += count($recs);
+        }
+        if ($recordCount > 0) {
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction', $publishData);
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_total', $recordCount);
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_processed', 0);
+        }
+        return $recordCount;
+    }
+
+    /**
+     * Initializes the command map to be used for flushing.
+     *
+     * @param int $workspace
+     * @param int $language
+     * @return int
+     */
+    protected function initFlushData($workspace, $language = null)
+    {
+        // workspace might be -98 a.k.a "All Workspaces but that's save here
+        $flushData = $this->getWorkspaceService()->getCmdArrayForFlushWS($workspace, true, 0, $language);
+        $recordCount = 0;
+        foreach ($flushData as $table => $recs) {
+            $recordCount += count($recs);
+        }
+        if ($recordCount > 0) {
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction', $flushData);
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_total', $recordCount);
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_processed', 0);
+        }
+        return $recordCount;
+    }
+
+    /**
+     * Processes the data.
+     *
+     * @param int $workspace
+     * @return int
+     */
+    protected function processData($workspace)
+    {
+        $processData = $GLOBALS['BE_USER']->getSessionData('workspaceMassAction');
+        $recordsProcessed = $GLOBALS['BE_USER']->getSessionData('workspaceMassAction_processed');
+        $limitedCmd = [];
+        $numRecs = 0;
+        foreach ($processData as $table => $recs) {
+            foreach ($recs as $key => $value) {
+                $numRecs++;
+                $limitedCmd[$table][$key] = $value;
+                if ($numRecs == self::MAX_RECORDS_TO_PROCESS) {
+                    break;
+                }
+            }
+            if ($numRecs == self::MAX_RECORDS_TO_PROCESS) {
+                break;
+            }
+        }
+        if ($numRecs == 0) {
+            // All done
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction', null);
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_total', 0);
+        } else {
+            /** @var $tce \TYPO3\CMS\Core\DataHandling\DataHandler */
+            $tce = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
+            // Execute the commands:
+            $tce->start([], $limitedCmd);
+            $tce->process_cmdmap();
+            $errors = $tce->errorLog;
+            if (!empty($errors)) {
+                throw new \Exception(implode(', ', $errors), 1476048278);
+            }
+            // Unset processed records
+            foreach ($limitedCmd as $table => $recs) {
+                foreach ($recs as $key => $value) {
+                    $recordsProcessed++;
+                    unset($processData[$table][$key]);
+                }
+            }
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction', $processData);
+            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_processed', $recordsProcessed);
+        }
+        return $recordsProcessed;
+    }
+}
diff --git a/typo3/sysext/workspaces/Classes/Controller/Remote/RemoteServer.php b/typo3/sysext/workspaces/Classes/Controller/Remote/RemoteServer.php
new file mode 100644 (file)
index 0000000..80c4501
--- /dev/null
@@ -0,0 +1,516 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Controller\Remote;
+
+/*
+ * 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\Backend\Avatar\Avatar;
+use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
+use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Html\RteHtmlParser;
+use TYPO3\CMS\Core\Imaging\Icon;
+use TYPO3\CMS\Core\Imaging\IconFactory;
+use TYPO3\CMS\Core\Resource\FileReference;
+use TYPO3\CMS\Core\Resource\ProcessedFile;
+use TYPO3\CMS\Core\Utility\DiffUtility;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\MathUtility;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
+use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
+use TYPO3\CMS\Lang\LanguageService;
+use TYPO3\CMS\Workspaces\Service\GridDataService;
+use TYPO3\CMS\Workspaces\Service\HistoryService;
+use TYPO3\CMS\Workspaces\Service\StagesService;
+use TYPO3\CMS\Workspaces\Service\WorkspaceService;
+
+/**
+ * Class RemoteServer
+ */
+class RemoteServer extends AbstractHandler
+{
+    /**
+     * @var GridDataService
+     */
+    protected $gridDataService;
+
+    /**
+     * @var StagesService
+     */
+    protected $stagesService;
+
+    /**
+     * @var DiffUtility
+     */
+    protected $differenceHandler;
+
+    /**
+     * Checks integrity of elements before peforming actions on them.
+     *
+     * @param \stdClass $parameters
+     * @return array
+     */
+    public function checkIntegrity(\stdClass $parameters)
+    {
+        $integrity = $this->createIntegrityService($this->getAffectedElements($parameters));
+        $integrity->check();
+        $response = [
+            'result' => $integrity->getStatusRepresentation()
+        ];
+        return $response;
+    }
+
+    /**
+     * Get List of workspace changes
+     *
+     * @param \stdClass $parameter
+     * @return array $data
+     */
+    public function getWorkspaceInfos($parameter)
+    {
+        // To avoid too much work we use -1 to indicate that every page is relevant
+        $pageId = $parameter->id > 0 ? $parameter->id : -1;
+        if (!isset($parameter->language) || !MathUtility::canBeInterpretedAsInteger($parameter->language)) {
+            $parameter->language = null;
+        }
+        $versions = $this->getWorkspaceService()->selectVersionsInWorkspace($this->getCurrentWorkspace(), 0, -99, $pageId, $parameter->depth, 'tables_select', $parameter->language);
+        $data = $this->getGridDataService()->generateGridListFromVersions($versions, $parameter, $this->getCurrentWorkspace());
+        return $data;
+    }
+
+    /**
+     * Get List of available workspace actions
+     *
+     * @param \stdClass $parameter
+     * @return array $data
+     */
+    public function getStageActions(\stdClass $parameter)
+    {
+        $currentWorkspace = $this->getCurrentWorkspace();
+        $stages = [];
+        if ($currentWorkspace != WorkspaceService::SELECT_ALL_WORKSPACES) {
+            $stages = $this->getStagesService()->getStagesForWSUser();
+        }
+        $data = [
+            'total' => count($stages),
+            'data' => $stages
+        ];
+        return $data;
+    }
+
+    /**
+     * Fetch further information to current selected workspace record.
+     *
+     * @param \stdClass $parameter
+     * @return array $data
+     */
+    public function getRowDetails($parameter)
+    {
+        $diffReturnArray = [];
+        $liveReturnArray = [];
+        $diffUtility = $this->getDifferenceHandler();
+        /** @var $parseObj RteHtmlParser */
+        $parseObj = GeneralUtility::makeInstance(RteHtmlParser::class);
+        $liveRecord = BackendUtility::getRecord($parameter->table, $parameter->t3ver_oid);
+        $versionRecord = BackendUtility::getRecord($parameter->table, $parameter->uid);
+        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
+        $icon_Live = $iconFactory->getIconForRecord($parameter->table, $liveRecord, Icon::SIZE_SMALL)->render();
+        $icon_Workspace = $iconFactory->getIconForRecord($parameter->table, $versionRecord, Icon::SIZE_SMALL)->render();
+        $stagesService = $this->getStagesService();
+        $stagePosition = $stagesService->getPositionOfCurrentStage($parameter->stage);
+        $fieldsOfRecords = array_keys($liveRecord);
+        if ($GLOBALS['TCA'][$parameter->table]) {
+            if ($GLOBALS['TCA'][$parameter->table]['interface']['showRecordFieldList']) {
+                $fieldsOfRecords = $GLOBALS['TCA'][$parameter->table]['interface']['showRecordFieldList'];
+                $fieldsOfRecords = GeneralUtility::trimExplode(',', $fieldsOfRecords, true);
+            }
+        }
+        foreach ($fieldsOfRecords as $fieldName) {
+            if (empty($GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'])) {
+                continue;
+            }
+            // Get the field's label. If not available, use the field name
+            $fieldTitle = $this->getLanguageService()->sL(BackendUtility::getItemLabel($parameter->table, $fieldName));
+            if (empty($fieldTitle)) {
+                $fieldTitle = $fieldName;
+            }
+            // Gets the TCA configuration for the current field
+            $configuration = $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'];
+            // check for exclude fields
+            if ($this->getBackendUser()->isAdmin() || $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['exclude'] == 0 || GeneralUtility::inList($this->getBackendUser()->groupData['non_exclude_fields'], $parameter->table . ':' . $fieldName)) {
+                // call diff class only if there is a difference
+                if ($configuration['type'] === 'inline' && $configuration['foreign_table'] === 'sys_file_reference') {
+                    $useThumbnails = false;
+                    if (!empty($configuration['foreign_selector_fieldTcaOverride']['config']['appearance']['elementBrowserAllowed']) && !empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'])) {
+                        $fileExtensions = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], true);
+                        $allowedExtensions = GeneralUtility::trimExplode(',', $configuration['foreign_selector_fieldTcaOverride']['config']['appearance']['elementBrowserAllowed'], true);
+                        $differentExtensions = array_diff($allowedExtensions, $fileExtensions);
+                        $useThumbnails = empty($differentExtensions);
+                    }
+
+                    $liveFileReferences = BackendUtility::resolveFileReferences(
+                        $parameter->table,
+                        $fieldName,
+                        $liveRecord,
+                        0
+                    );
+                    $versionFileReferences = BackendUtility::resolveFileReferences(
+                        $parameter->table,
+                        $fieldName,
+                        $versionRecord,
+                        $this->getCurrentWorkspace()
+                    );
+                    $fileReferenceDifferences = $this->prepareFileReferenceDifferences(
+                        $liveFileReferences,
+                        $versionFileReferences,
+                        $useThumbnails
+                    );
+
+                    if ($fileReferenceDifferences === null) {
+                        continue;
+                    }
+
+                    $diffReturnArray[] = [
+                        'field' => $fieldName,
+                        'label' => $fieldTitle,
+                        'content' => $fileReferenceDifferences['differences']
+                    ];
+                    $liveReturnArray[] = [
+                        'field' => $fieldName,
+                        'label' => $fieldTitle,
+                        'content' => $fileReferenceDifferences['live']
+                    ];
+                } elseif ((string)$liveRecord[$fieldName] !== (string)$versionRecord[$fieldName]) {
+                    // Select the human readable values before diff
+                    $liveRecord[$fieldName] = BackendUtility::getProcessedValue(
+                        $parameter->table,
+                        $fieldName,
+                        $liveRecord[$fieldName],
+                        0,
+                        1,
+                        false,
+                        $liveRecord['uid']
+                    );
+                    $versionRecord[$fieldName] = BackendUtility::getProcessedValue(
+                        $parameter->table,
+                        $fieldName,
+                        $versionRecord[$fieldName],
+                        0,
+                        1,
+                        false,
+                        $versionRecord['uid']
+                    );
+
+                    if ($configuration['type'] == 'group' && $configuration['internal_type'] == 'file') {
+                        $versionThumb = BackendUtility::thumbCode($versionRecord, $parameter->table, $fieldName, '');
+                        $liveThumb = BackendUtility::thumbCode($liveRecord, $parameter->table, $fieldName, '');
+                        $diffReturnArray[] = [
+                            'field' => $fieldName,
+                            'label' => $fieldTitle,
+                            'content' => $versionThumb
+                        ];
+                        $liveReturnArray[] = [
+                            'field' => $fieldName,
+                            'label' => $fieldTitle,
+                            'content' => $liveThumb
+                        ];
+                    } else {
+                        $diffReturnArray[] = [
+                            'field' => $fieldName,
+                            'label' => $fieldTitle,
+                            'content' => $diffUtility->makeDiffDisplay($liveRecord[$fieldName], $versionRecord[$fieldName])
+                        ];
+                        $liveReturnArray[] = [
+                            'field' => $fieldName,
+                            'label' => $fieldTitle,
+                            'content' => $parseObj->TS_images_rte($liveRecord[$fieldName])
+                        ];
+                    }
+                }
+            }
+        }
+        // Hook for modifying the difference and live arrays
+        // (this may be used by custom or dynamically-defined fields)
+        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'])) {
+            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] as $className) {
+                $hookObject = GeneralUtility::getUserObj($className);
+                if (method_exists($hookObject, 'modifyDifferenceArray')) {
+                    $hookObject->modifyDifferenceArray($parameter, $diffReturnArray, $liveReturnArray, $diffUtility);
+                }
+            }
+        }
+        $commentsForRecord = $this->getCommentsForRecord($parameter->uid, $parameter->table);
+
+        /** @var $historyService HistoryService */
+        $historyService = GeneralUtility::makeInstance(HistoryService::class);
+        $history = $historyService->getHistory($parameter->table, $parameter->t3ver_oid);
+
+        $prevStage = $stagesService->getPrevStage($parameter->stage);
+        $nextStage = $stagesService->getNextStage($parameter->stage);
+
+        if (isset($prevStage[0])) {
+            $prevStage = current($prevStage);
+        }
+
+        if (isset($nextStage[0])) {
+            $nextStage = current($nextStage);
+        }
+
+        return [
+            'total' => 1,
+            'data' => [
+                [
+                    // these parts contain HTML (don't escape)
+                    'diff' => $diffReturnArray,
+                    'live_record' => $liveReturnArray,
+                    'icon_Live' => $icon_Live,
+                    'icon_Workspace' => $icon_Workspace,
+                    // this part is already escaped in getCommentsForRecord()
+                    'comments' => $commentsForRecord,
+                    // escape/sanitize the others
+                    'path_Live' => htmlspecialchars(BackendUtility::getRecordPath($liveRecord['pid'], '', 999)),
+                    'label_Stage' => htmlspecialchars($stagesService->getStageTitle($parameter->stage)),
+                    'label_PrevStage' => $prevStage,
+                    'label_NextStage' => $nextStage,
+                    'stage_position' => (int)$stagePosition['position'],
+                    'stage_count' => (int)$stagePosition['count'],
+                    'parent' => [
+                        'table' => htmlspecialchars($parameter->table),
+                        'uid' => (int)$parameter->uid
+                    ],
+                    'history' => [
+                        'data' => $history,
+                        'total' => count($history)
+                    ]
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * Prepares difference view for file references.
+     *
+     * @param FileReference[] $liveFileReferences
+     * @param FileReference[] $versionFileReferences
+     * @param bool|false $useThumbnails
+     * @return array|null
+     */
+    protected function prepareFileReferenceDifferences(array $liveFileReferences, array $versionFileReferences, $useThumbnails = false)
+    {
+        $randomValue = uniqid('file');
+
+        $liveValues = [];
+        $versionValues = [];
+        $candidates = [];
+        $substitutes = [];
+
+        // Process live references
+        foreach ($liveFileReferences as $identifier => $liveFileReference) {
+            $identifierWithRandomValue = $randomValue . '__' . $liveFileReference->getUid() . '__' . $randomValue;
+            $candidates[$identifierWithRandomValue] = $liveFileReference;
+            $liveValues[] = $identifierWithRandomValue;
+        }
+
+        // Process version references
+        foreach ($versionFileReferences as $identifier => $versionFileReference) {
+            $identifierWithRandomValue = $randomValue . '__' . $versionFileReference->getUid() . '__' . $randomValue;
+            $candidates[$identifierWithRandomValue] = $versionFileReference;
+            $versionValues[] = $identifierWithRandomValue;
+        }
+
+        // Combine values and surround by spaces
+        // (to reduce the chunks Diff will find)
+        $liveInformation = ' ' . implode(' ', $liveValues) . ' ';
+        $versionInformation = ' ' . implode(' ', $versionValues) . ' ';
+
+        // Return if information has not changed
+        if ($liveInformation === $versionInformation) {
+            return null;
+        }
+
+        /**
+         * @var string $identifierWithRandomValue
+         * @var FileReference $fileReference
+         */
+        foreach ($candidates as $identifierWithRandomValue => $fileReference) {
+            if ($useThumbnails) {
+                $thumbnailFile = $fileReference->getOriginalFile()->process(
+                    ProcessedFile::CONTEXT_IMAGEPREVIEW,
+                    ['width' => 40, 'height' => 40]
+                );
+                $thumbnailMarkup = '<img src="' . $thumbnailFile->getPublicUrl(true) . '" />';
+                $substitutes[$identifierWithRandomValue] = $thumbnailMarkup;
+            } else {
+                $substitutes[$identifierWithRandomValue] = $fileReference->getPublicUrl();
+            }
+        }
+
+        $differences = $this->getDifferenceHandler()->makeDiffDisplay($liveInformation, $versionInformation);
+        $liveInformation = str_replace(array_keys($substitutes), array_values($substitutes), trim($liveInformation));
+        $differences = str_replace(array_keys($substitutes), array_values($substitutes), trim($differences));
+
+        return [
+            'live' => $liveInformation,
+            'differences' => $differences
+        ];
+    }
+
+    /**
+     * Gets an array with all sys_log entries and their comments for the given record uid and table
+     *
+     * @param int $uid uid of changed element to search for in log
+     * @param string $table Name of the record's table
+     * @return array
+     */
+    public function getCommentsForRecord($uid, $table)
+    {
+        $sysLogReturnArray = [];
+        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
+
+        $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($table, \PDO::PARAM_STR)
+                ),
+                $queryBuilder->expr()->eq(
+                    'recuid',
+                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
+                )
+            )
+            ->orderBy('tstamp', 'DESC')
+            ->execute();
+
+        /** @var Avatar $avatar */
+        $avatar = GeneralUtility::makeInstance(Avatar::class);
+
+        while ($sysLogRow = $result->fetch()) {
+            $sysLogEntry = [];
+            $data = unserialize($sysLogRow['log_data']);
+            $beUserRecord = BackendUtility::getRecord('be_users', $sysLogRow['userid']);
+            $sysLogEntry['stage_title'] = htmlspecialchars($this->getStagesService()->getStageTitle($data['stage']));
+            $sysLogEntry['user_uid'] = (int)$sysLogRow['userid'];
+            $sysLogEntry['user_username'] = is_array($beUserRecord) ? htmlspecialchars($beUserRecord['username']) : '';
+            $sysLogEntry['tstamp'] = htmlspecialchars(BackendUtility::datetime($sysLogRow['tstamp']));
+            $sysLogEntry['user_comment'] = nl2br(htmlspecialchars($data['comment']));
+            $sysLogEntry['user_avatar'] = $avatar->render($beUserRecord);
+            $sysLogReturnArray[] = $sysLogEntry;
+        }
+        return $sysLogReturnArray;
+    }
+
+    /**
+     * Gets all available system languages.
+     *
+     * @return array
+     */
+    public function getSystemLanguages()
+    {
+        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
+        $systemLanguages = [
+            [
+                'uid' => 'all',
+                'title' => LocalizationUtility::translate('language.allLanguages', 'workspaces'),
+                'icon' => $iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render()
+            ]
+        ];
+        foreach ($this->getGridDataService()->getSystemLanguages() as $id => $systemLanguage) {
+            if ($id < 0) {
+                continue;
+            }
+            $systemLanguages[] = [
+                'uid' => $id,
+                'title' => htmlspecialchars($systemLanguage['title']),
+                'icon' => $iconFactory->getIcon($systemLanguage['flagIcon'], Icon::SIZE_SMALL)->render()
+            ];
+        }
+        $result = [
+            'total' => count($systemLanguages),
+            'data' => $systemLanguages
+        ];
+        return $result;
+    }
+
+    /**
+     * @return BackendUserAuthentication;
+     */
+    protected function getBackendUser()
+    {
+        return $GLOBALS['BE_USER'];
+    }
+
+    /**
+     * @return LanguageService;
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
+
+    /**
+     * Gets the Grid Data Service.
+     *
+     * @return GridDataService
+     */
+    protected function getGridDataService()
+    {
+        if (!isset($this->gridDataService)) {
+            $this->gridDataService = GeneralUtility::makeInstance(GridDataService::class);
+        }
+        return $this->gridDataService;
+    }
+
+    /**
+     * Gets the Stages Service.
+     *
+     * @return StagesService
+     */
+    protected function getStagesService()
+    {
+        if (!isset($this->stagesService)) {
+            $this->stagesService = GeneralUtility::makeInstance(StagesService::class);
+        }
+        return $this->stagesService;
+    }
+
+    /**
+     * Gets the difference handler, parsing differences based on sentences.
+     *
+     * @return DiffUtility
+     */
+    protected function getDifferenceHandler()
+    {
+        if (!isset($this->differenceHandler)) {
+            $this->differenceHandler = GeneralUtility::makeInstance(DiffUtility::class);
+        }
+        return $this->differenceHandler;
+    }
+
+    /**
+     * @return \TYPO3\CMS\Extbase\Object\ObjectManager
+     */
+    protected function getObjectManager()
+    {
+        return GeneralUtility::makeInstance(ObjectManager::class);
+    }
+}
index af5e104..7dd689a 100644 (file)
@@ -16,7 +16,6 @@ namespace TYPO3\CMS\Workspaces\Controller;
 
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
 use TYPO3\CMS\Core\Imaging\Icon;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
@@ -201,14 +200,8 @@ class ReviewController extends AbstractController
     protected function initializeAction()
     {
         parent::initializeAction();
-        $this->pageRenderer->addJsFile('EXT:backend/Resources/Public/JavaScript/ExtDirect.StateProvider.js');
-        $this->pageRenderer->loadExtJS(false, false);
         $states = $this->getBackendUser()->uc['moduleData']['Workspaces']['States'];
         $this->pageRenderer->addInlineSetting('Workspaces', 'States', $states);
-        // Load  JavaScript:
-        $this->pageRenderer->addExtDirectCode([
-            'TYPO3.Workspaces'
-        ]);
 
         foreach ($this->getAdditionalResourceService()->getLocalizationResources() as $localizationResource) {
             $this->pageRenderer->addInlineLanguageLabelFile($localizationResource);
@@ -216,7 +209,6 @@ class ReviewController extends AbstractController
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Workspaces/Backend');
         $this->pageRenderer->addInlineSetting('FormEngine', 'moduleUrl', BackendUtility::getModuleUrl('record_edit'));
         $this->pageRenderer->addInlineSetting('RecordHistory', 'moduleUrl', BackendUtility::getModuleUrl('record_history'));
-        $this->pageRenderer->addInlineSetting('Workspaces', 'token', FormProtectionFactory::get('backend')->generateToken('extDirect'));
         $this->pageRenderer->addInlineSetting('Workspaces', 'id', (int)GeneralUtility::_GP('id'));
     }
 
diff --git a/typo3/sysext/workspaces/Classes/ExtDirect/AbstractHandler.php b/typo3/sysext/workspaces/Classes/ExtDirect/AbstractHandler.php
deleted file mode 100644 (file)
index 64f3fe5..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-namespace TYPO3\CMS\Workspaces\ExtDirect;
-
-/*
- * 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!
- */
-
-/**
- * Abstract ExtDirect handler
- */
-abstract class AbstractHandler
-{
-    /**
-     * Gets the current workspace ID.
-     *
-     * @return int The current workspace ID
-     */
-    protected function getCurrentWorkspace()
-    {
-        return $this->getWorkspaceService()->getCurrentWorkspace();
-    }
-
-    /**
-     * Gets an error response to be shown in the grid component.
-     *
-     * @param string $errorLabel Name of the label in the locallang.xlf file
-     * @param int $errorCode The error code to be used
-     * @param bool $successFlagValue Value of the success flag to be delivered back (might be FALSE in most cases)
-     * @return array
-     */
-    protected function getErrorResponse($errorLabel, $errorCode = 0, $successFlagValue = false)
-    {
-        $localLangFile = 'LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf';
-        $response = [
-            'error' => [
-                'code' => $errorCode,
-                'message' => $GLOBALS['LANG']->sL($localLangFile . ':' . $errorLabel)
-            ],
-            'success' => $successFlagValue
-        ];
-        return $response;
-    }
-
-    /**
-     * Gets an instance of the workspaces service.
-     *
-     * @return \TYPO3\CMS\Workspaces\Service\WorkspaceService
-     */
-    protected function getWorkspaceService()
-    {
-        return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class);
-    }
-
-    /**
-     * Validates whether the submitted language parameter can be
-     * interpreted as integer value.
-     *
-     * @param stdClass $parameters
-     * @return int|NULL
-     */
-    protected function validateLanguageParameter(\stdClass $parameters)
-    {
-        $language = null;
-        if (isset($parameters->language) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($parameters->language)) {
-            $language = $parameters->language;
-        }
-        return $language;
-    }
-
-    /**
-     * Gets affected elements on publishing/swapping actions.
-     * Affected elements have a dependency, e.g. translation overlay
-     * and the default origin record - thus, the default record would be
-     * affected if the translation overlay shall be published.
-     *
-     * @param stdClass $parameters
-     * @return array
-     */
-    protected function getAffectedElements(\stdClass $parameters)
-    {
-        $affectedElements = [];
-        if ($parameters->type === 'selection') {
-            foreach ((array)$parameters->selection as $element) {
-                $affectedElements[] = \TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord::create($element->table, $element->liveId, $element->versionId);
-            }
-        } elseif ($parameters->type === 'all') {
-            $versions = $this->getWorkspaceService()->selectVersionsInWorkspace($this->getCurrentWorkspace(), 0, -99, -1, 0, 'tables_select', $this->validateLanguageParameter($parameters));
-            foreach ($versions as $table => $tableElements) {
-                foreach ($tableElements as $element) {
-                    $affectedElement = \TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord::create($table, $element['t3ver_oid'], $element['uid']);
-                    $affectedElement->getVersionRecord()->setRow($element);
-                    $affectedElements[] = $affectedElement;
-                }
-            }
-        }
-        return $affectedElements;
-    }
-
-    /**
-     * Creates a new instance of the integrity service for the
-     * given set of affected elements.
-     *
-     * @param \TYPO3\CMS\Workspaces\Domain\Model\CombinedRecord[] $affectedElements
-     * @return \TYPO3\CMS\Workspaces\Service\IntegrityService
-     * @see getAffectedElements
-     */
-    protected function createIntegrityService(array $affectedElements)
-    {
-        /** @var $integrityService \TYPO3\CMS\Workspaces\Service\IntegrityService */
-        $integrityService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\IntegrityService::class);
-        $integrityService->setAffectedElements($affectedElements);
-        return $integrityService;
-    }
-}
diff --git a/typo3/sysext/workspaces/Classes/ExtDirect/ActionHandler.php b/typo3/sysext/workspaces/Classes/ExtDirect/ActionHandler.php
deleted file mode 100644 (file)
index c57b930..0000000
+++ /dev/null
@@ -1,854 +0,0 @@
-<?php
-namespace TYPO3\CMS\Workspaces\ExtDirect;
-
-/*
- * 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\ExtensionManagementUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Fluid\View\StandaloneView;
-use TYPO3\CMS\Workspaces\Domain\Record\StageRecord;
-use TYPO3\CMS\Workspaces\Domain\Record\WorkspaceRecord;
-use TYPO3\CMS\Workspaces\Service\StagesService;
-use TYPO3\CMS\Workspaces\Service\WorkspaceService;
-
-/**
- * ExtDirect action handler
- */
-class ActionHandler extends AbstractHandler
-{
-    /**
-     * @var StagesService
-     */
-    protected $stageService;
-
-    /**
-     * Creates this object.
-     */
-    public function __construct()
-    {
-        $this->stageService = GeneralUtility::makeInstance(StagesService::class);
-    }
-
-    /**
-     * Generates a workspace preview link.
-     *
-     * @param int $uid The ID of the record to be linked
-     * @return string the full domain including the protocol http:// or https://, but without the trailing '/'
-     */
-    public function generateWorkspacePreviewLink($uid)
-    {
-        return $this->getWorkspaceService()->generateWorkspacePreviewLink($uid);
-    }
-
-    /**
-     * Generates workspace preview links for all available languages of a page.
-     *
-     * @param int $uid
-     * @return array
-     */
-    public function generateWorkspacePreviewLinksForAllLanguages($uid)
-    {
-        return $this->getWorkspaceService()->generateWorkspacePreviewLinksForAllLanguages($uid);
-    }
-
-    /**
-     * Swaps a single record.
-     *
-     * @param string $table
-     * @param int $t3ver_oid
-     * @param int $orig_uid
-     * @return void
-     * @todo What about reporting errors back to the ExtJS interface? /olly/
-     */
-    public function swapSingleRecord($table, $t3ver_oid, $orig_uid)
-    {
-        $versionRecord = BackendUtility::getRecord($table, $orig_uid);
-        $currentWorkspace = $this->setTemporaryWorkspace($versionRecord['t3ver_wsid']);
-
-        $cmd[$table][$t3ver_oid]['version'] = [
-            'action' => 'swap',
-            'swapWith' => $orig_uid,
-            'swapIntoWS' => 1
-        ];
-        $this->processTcaCmd($cmd);
-
-        $this->setTemporaryWorkspace($currentWorkspace);
-    }
-
-    /**
-     * Deletes a single record.
-     *
-     * @param string $table
-     * @param int $uid
-     * @return void
-     * @todo What about reporting errors back to the ExtJS interface? /olly/
-     */
-    public function deleteSingleRecord($table, $uid)
-    {
-        $versionRecord = BackendUtility::getRecord($table, $uid);
-        $currentWorkspace = $this->setTemporaryWorkspace($versionRecord['t3ver_wsid']);
-
-        $cmd[$table][$uid]['version'] = [
-            'action' => 'clearWSID'
-        ];
-        $this->processTcaCmd($cmd);
-
-        $this->setTemporaryWorkspace($currentWorkspace);
-    }
-
-    /**
-     * Generates a view link for a page.
-     *
-     * @param string $table
-     * @param string $uid
-     * @return string
-     */
-    public function viewSingleRecord($table, $uid)
-    {
-        return WorkspaceService::viewSingleRecord($table, $uid);
-    }
-
-    /**
-     * Executes an action (publish, discard, swap) to a selection set.
-     *
-     * @param \stdClass $parameter
-     * @return array
-     */
-    public function executeSelectionAction($parameter)
-    {
-        $result = [];
-
-        if (empty($parameter->action) || empty($parameter->selection)) {
-            $result['error'] = 'No action or record selection given';
-            return $result;
-        }
-
-        $commands = [];
-        $swapIntoWorkspace = ($parameter->action === 'swap');
-        if ($parameter->action === 'publish' || $swapIntoWorkspace) {
-            $commands = $this->getPublishSwapCommands($parameter->selection, $swapIntoWorkspace);
-        } elseif ($parameter->action === 'discard') {
-            $commands = $this->getFlushCommands($parameter->selection);
-        }
-
-        $result = $this->processTcaCmd($commands);
-        $result['total'] = count($commands);
-        return $result;
-    }
-
-    /**
-     * Get publish swap commands
-     *
-     * @param array|\stdClass[] $selection
-     * @param bool $swapIntoWorkspace
-     * @return array
-     */
-    protected function getPublishSwapCommands(array $selection, $swapIntoWorkspace)
-    {
-        $commands = [];
-        foreach ($selection as $record) {
-            $commands[$record->table][$record->liveId]['version'] = [
-                'action' => 'swap',
-                'swapWith' => $record->versionId,
-                'swapIntoWS' => (bool)$swapIntoWorkspace,
-            ];
-        }
-        return $commands;
-    }
-
-    /**
-     * Get flush commands
-     *
-     * @param array|\stdClass[] $selection
-     * @return array
-     */
-    protected function getFlushCommands(array $selection)
-    {
-        $commands = [];
-        foreach ($selection as $record) {
-            $commands[$record->table][$record->versionId]['version'] = [
-                'action' => 'clearWSID',
-            ];
-        }
-        return $commands;
-    }
-
-    /**
-     * Saves the selected columns to be shown to the preferences of the current backend user.
-     *
-     * @param \stdClass $model
-     * @return void
-     */
-    public function saveColumnModel($model)
-    {
-        $data = [];
-        foreach ($model as $column) {
-            $data[$column->column] = [
-                'position' => $column->position,
-                'hidden' => $column->hidden
-            ];
-        }
-        $GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['columns'] = $data;
-        $GLOBALS['BE_USER']->writeUC();
-    }
-
-    public function loadColumnModel()
-    {
-        if (is_array($GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['columns'])) {
-            return $GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['columns'];
-        } else {
-            return [];
-        }
-    }
-
-    /**
-     * Saves the selected language.
-     *
-     * @param int|string $language
-     * @return void
-     */
-    public function saveLanguageSelection($language)
-    {
-        if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($language) === false && $language !== 'all') {
-            $language = 'all';
-        }
-        $GLOBALS['BE_USER']->uc['moduleData']['Workspaces'][$GLOBALS['BE_USER']->workspace]['language'] = $language;
-        $GLOBALS['BE_USER']->writeUC();
-    }
-
-    /**
-     * Gets the dialog window to be displayed before a record can be sent to the next stage.
-     *
-     * @param int $uid
-     * @param string $table
-     * @param int $t3ver_oid
-     * @return array
-     */
-    public function sendToNextStageWindow($uid, $table, $t3ver_oid)
-    {
-        $elementRecord = BackendUtility::getRecord($table, $uid);
-        $currentWorkspace = $this->setTemporaryWorkspace($elementRecord['t3ver_wsid']);
-
-        if (is_array($elementRecord)) {
-            $workspaceRecord = WorkspaceRecord::get($elementRecord['t3ver_wsid']);
-            $nextStageRecord = $workspaceRecord->getNextStage($elementRecord['t3ver_stage']);
-            if ($nextStageRecord !== null) {
-                $this->stageService->getRecordService()->add($table, $uid);
-                $result = $this->getSentToStageWindow($nextStageRecord);
-                $result['affects'] = [
-                    'table' => $table,
-                    'nextStage' => $nextStageRecord->getUid(),
-                    't3ver_oid' => $t3ver_oid,
-                    'uid' => $uid
-                ];
-            } else {
-                $result = $this->getErrorResponse('error.stageId.invalid', 1291111644);
-            }
-        } else {
-            $result = $this->getErrorResponse('error.sendToNextStage.noRecordFound', 1287264776);
-        }
-
-        $this->setTemporaryWorkspace($currentWorkspace);
-        return $result;
-    }
-
-    /**
-     * Gets the dialog window to be displayed before a record can be sent to the previous stage.
-     *
-     * @param int $uid
-     * @param string $table
-     * @return array
-     */
-    public function sendToPrevStageWindow($uid, $table)
-    {
-        $elementRecord = BackendUtility::getRecord($table, $uid);
-        $currentWorkspace = $this->setTemporaryWorkspace($elementRecord['t3ver_wsid']);
-
-        if (is_array($elementRecord)) {
-            $workspaceRecord = WorkspaceRecord::get($elementRecord['t3ver_wsid']);
-            $stageRecord = $workspaceRecord->getStage($elementRecord['t3ver_stage']);
-
-            if ($stageRecord !== null) {
-                if (!$stageRecord->isEditStage()) {
-                    $this->stageService->getRecordService()->add($table, $uid);
-                    $previousStageRecord = $stageRecord->getPrevious();
-                    $result = $this->getSentToStageWindow($previousStageRecord);
-                    $result['affects'] = [
-                        'table' => $table,
-                        'uid' => $uid,
-                        'nextStage' => $previousStageRecord->getUid()
-                    ];
-                } else {
-                    // element is already in edit stage, there is no prev stage - return an error message
-                    $result = $this->getErrorResponse('error.sendToPrevStage.noPreviousStage', 1287264746);
-                }
-            } else {
-                $result = $this->getErrorResponse('error.stageId.invalid', 1291111644);
-            }
-        } else {
-            $result = $this->getErrorResponse('error.sendToNextStage.noRecordFound', 1287264765);
-        }
-
-        $this->setTemporaryWorkspace($currentWorkspace);
-        return $result;
-    }
-
-    /**
-     * Gets the dialog window to be displayed before a record can be sent to a specific stage.
-     *
-     * @param int $nextStageId
-     * @param array|\stdClass[] $elements
-     * @return array
-     */
-    public function sendToSpecificStageWindow($nextStageId, array $elements)
-    {
-        foreach ($elements as $element) {
-            $this->stageService->getRecordService()->add(
-                $element->table,
-                $element->uid
-            );
-        }
-
-        $result = $this->getSentToStageWindow($nextStageId);
-        $result['affects'] = [
-            'nextStage' => $nextStageId
-        ];
-        return $result;
-    }
-
-    /**
-     * Gets a merged variant of recipient defined by uid and custom ones.
-     *
-     * @param array $uidOfRecipients list of recipients
-     * @param string $additionalRecipients given user string of additional recipients
-     * @param int $stageId stage id
-     * @return array
-     * @throws \InvalidArgumentException
-     */
-    public function getRecipientList(array $uidOfRecipients, $additionalRecipients, $stageId)
-    {
-        $stageRecord = WorkspaceRecord::get($this->getCurrentWorkspace())->getStage($stageId);
-
-        if ($stageRecord === null) {
-            throw new \InvalidArgumentException(
-                $GLOBALS['LANG']->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf:error.stageId.integer'),
-                1476044776
-            );
-        }
-
-        $recipients = [];
-        $finalRecipients = [];
-        $backendUserIds = $stageRecord->getAllRecipients();
-        foreach ($uidOfRecipients as $userUid) {
-            // Ensure that only configured backend users are considered
-            if (!in_array($userUid, $backendUserIds)) {
-                continue;
-            }
-            $beUserRecord = BackendUtility::getRecord('be_users', (int)$userUid);
-            if (is_array($beUserRecord) && $beUserRecord['email'] !== '') {
-                $uc = $beUserRecord['uc'] ? unserialize($beUserRecord['uc']) : [];
-                $recipients[$beUserRecord['email']] = [
-                    'email' => $beUserRecord['email'],
-                    'lang' => isset($uc['lang']) ? $uc['lang'] : $beUserRecord['lang']
-                ];
-            }
-        }
-
-        if ($stageRecord->hasPreselection() && !$stageRecord->isPreselectionChangeable()) {
-            $preselectedBackendUsers = $this->getStageService()->getBackendUsers(
-                implode(',', $this->stageService->getPreselectedRecipients($stageRecord))
-            );
-
-            foreach ($preselectedBackendUsers as $preselectedBackendUser) {
-                if (empty($preselectedBackendUser['email']) || !GeneralUtility::validEmail($preselectedBackendUser['email'])) {
-                    continue;
-                }
-                if (!isset($recipients[$preselectedBackendUser['email']])) {
-                    $uc = (!empty($preselectedBackendUser['uc']) ? unserialize($preselectedBackendUser['uc']) : []);
-                    $recipients[$preselectedBackendUser['email']] = [
-                        'email' => $preselectedBackendUser['email'],
-                        'lang' => (isset($uc['lang']) ? $uc['lang'] : $preselectedBackendUser['lang'])
-                    ];
-                }
-            }
-        }
-
-        if ($additionalRecipients !== '') {
-            $emails = GeneralUtility::trimExplode(LF, $additionalRecipients, true);
-            $additionalRecipients = [];
-            foreach ($emails as $email) {
-                $additionalRecipients[$email] = ['email' => $email];
-            }
-        } else {
-            $additionalRecipients = [];
-        }
-        // We merge $recipients on top of $additionalRecipients because $recipients
-        // possibly is more complete with a user language. Furthermore, the list of
-        // recipients is automatically unique since we indexed $additionalRecipients
-        // and $recipients with the email address
-        $allRecipients = array_merge($additionalRecipients, $recipients);
-        foreach ($allRecipients as $email => $recipientInformation) {
-            if (GeneralUtility::validEmail($email)) {
-                $finalRecipients[] = $recipientInformation;
-            }
-        }
-        return $finalRecipients;
-    }
-
-    /**
-     * Discard all items from given page id.
-     *
-     * @param int $pageId
-     * @return array
-     */
-    public function discardStagesFromPage($pageId)
-    {
-        $cmdMapArray = [];
-        /** @var $workspaceService WorkspaceService */
-        $workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
-        /** @var $stageService StagesService */
-        $stageService = GeneralUtility::makeInstance(StagesService::class);
-        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($stageService->getWorkspaceId(), ($filter = 1), ($stage = -99), $pageId, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
-        foreach ($workspaceItemsArray as $tableName => $items) {
-            foreach ($items as $item) {
-                $cmdMapArray[$tableName][$item['uid']]['version']['action'] = 'clearWSID';
-            }
-        }
-        $this->processTcaCmd($cmdMapArray);
-        return [
-            'success' => true
-        ];
-    }
-
-    /**
-     * Push the given element collection to the next workspace stage.
-     *
-     * <code>
-     * $parameters->additional = your@mail.com
-     * $parameters->affects->__TABLENAME__
-     * $parameters->comments
-     * $parameters->receipients
-     * $parameters->stageId
-     * </code>
-     *
-     * @param \stdClass $parameters
-     * @return array
-     */
-    public function sentCollectionToStage(\stdClass $parameters)
-    {
-        $cmdMapArray = [];
-        $comment = $parameters->comments;
-        $stageId = $parameters->stageId;
-        if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($stageId) === false) {
-            throw new \InvalidArgumentException('Missing "stageId" in $parameters array.', 1319488194);
-        }
-        if (!is_object($parameters->affects) || empty($parameters->affects)) {
-            throw new \InvalidArgumentException('Missing "affected items" in $parameters array.', 1319488195);
-        }
-        $recipients = $this->getRecipientList((array)$parameters->recipients, $parameters->additional, $stageId);
-        foreach ($parameters->affects as $tableName => $items) {
-            foreach ($items as $item) {
-                // Publishing uses live id in command map
-                if ($stageId == StagesService::STAGE_PUBLISH_EXECUTE_ID) {
-                    $cmdMapArray[$tableName][$item->t3ver_oid]['version']['action'] = 'swap';
-                    $cmdMapArray[$tableName][$item->t3ver_oid]['version']['swapWith'] = $item->uid;
-                    $cmdMapArray[$tableName][$item->t3ver_oid]['version']['comment'] = $comment;
-                    $cmdMapArray[$tableName][$item->t3ver_oid]['version']['notificationAlternativeRecipients'] = $recipients;
-                // Setting stage uses version id in command map
-                } else {
-                    $cmdMapArray[$tableName][$item->uid]['version']['action'] = 'setStage';
-                    $cmdMapArray[$tableName][$item->uid]['version']['stageId'] = $stageId;
-                    $cmdMapArray[$tableName][$item->uid]['version']['comment'] = $comment;
-                    $cmdMapArray[$tableName][$item->uid]['version']['notificationAlternativeRecipients'] = $recipients;
-                }
-            }
-        }
-        $this->processTcaCmd($cmdMapArray);
-        return [
-            'success' => true,
-            // force refresh after publishing changes
-            'refreshLivePanel' => $parameters->stageId == -20
-        ];
-    }
-
-    /**
-     * Process TCA command map array.
-     *
-     * @param array $cmdMapArray
-     * @return array
-     */
-    protected function processTcaCmd(array $cmdMapArray)
-    {
-        $result = [];
-
-        if (empty($cmdMapArray)) {
-            $result['error'] = 'No commands given to be processed';
-            return $result;
-        }
-
-        /** @var \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler */
-        $dataHandler = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
-        $dataHandler->start([], $cmdMapArray);
-        $dataHandler->process_cmdmap();
-
-        if ($dataHandler->errorLog) {
-            $result['error'] = implode('<br/>', $dataHandler->errorLog);
-        }
-
-        return $result;
-    }
-
-    /**
-     * Gets an object with this structure:
-     *
-     * affects: object
-     * table
-     * t3ver_oid
-     * nextStage
-     * uid
-     * receipients: array with uids
-     * additional: string
-     * comments: string
-     *
-     * @param \stdClass $parameters
-     * @return array
-     */
-    public function sendToNextStageExecute(\stdClass $parameters)
-    {
-        $cmdArray = [];
-        $setStageId = $parameters->affects->nextStage;
-        $comments = $parameters->comments;
-        $table = $parameters->affects->table;
-        $uid = $parameters->affects->uid;
-        $t3ver_oid = $parameters->affects->t3ver_oid;
-
-        $elementRecord = BackendUtility::getRecord($table, $uid);
-        $currentWorkspace = $this->setTemporaryWorkspace($elementRecord['t3ver_wsid']);
-
-        $recipients = $this->getRecipientList((array)$parameters->recipients, $parameters->additional, $setStageId);
-        if ($setStageId == StagesService::STAGE_PUBLISH_EXECUTE_ID) {
-            $cmdArray[$table][$t3ver_oid]['version']['action'] = 'swap';
-            $cmdArray[$table][$t3ver_oid]['version']['swapWith'] = $uid;
-            $cmdArray[$table][$t3ver_oid]['version']['comment'] = $comments;
-            $cmdArray[$table][$t3ver_oid]['version']['notificationAlternativeRecipients'] = $recipients;
-        } else {
-            $cmdArray[$table][$uid]['version']['action'] = 'setStage';
-            $cmdArray[$table][$uid]['version']['stageId'] = $setStageId;
-            $cmdArray[$table][$uid]['version']['comment'] = $comments;
-            $cmdArray[$table][$uid]['version']['notificationAlternativeRecipients'] = $recipients;
-        }
-        $this->processTcaCmd($cmdArray);
-        $result = [
-            'success' => true
-        ];
-
-        $this->setTemporaryWorkspace($currentWorkspace);
-        return $result;
-    }
-
-    /**
-     * Gets an object with this structure:
-     *
-     * affects: object
-     * table
-     * t3ver_oid
-     * nextStage
-     * receipients: array with uids
-     * additional: string
-     * comments: string
-     *
-     * @param \stdClass $parameters
-     * @return array
-     */
-    public function sendToPrevStageExecute(\stdClass $parameters)
-    {
-        $cmdArray = [];
-        $setStageId = $parameters->affects->nextStage;
-        $comments = $parameters->comments;
-        $table = $parameters->affects->table;
-        $uid = $parameters->affects->uid;
-
-        $elementRecord = BackendUtility::getRecord($table, $uid);
-        $currentWorkspace = $this->setTemporaryWorkspace($elementRecord['t3ver_wsid']);
-
-        $recipients = $this->getRecipientList((array)$parameters->recipients, $parameters->additional, $setStageId);
-        $cmdArray[$table][$uid]['version']['action'] = 'setStage';
-        $cmdArray[$table][$uid]['version']['stageId'] = $setStageId;
-        $cmdArray[$table][$uid]['version']['comment'] = $comments;
-        $cmdArray[$table][$uid]['version']['notificationAlternativeRecipients'] = $recipients;
-        $this->processTcaCmd($cmdArray);
-        $result = [
-            'success' => true
-        ];
-
-        $this->setTemporaryWorkspace($currentWorkspace);
-        return $result;
-    }
-
-    /**
-     * Gets an object with this structure:
-     *
-     * affects: object
-     * elements: array
-     * 0: object
-     * table
-     * t3ver_oid
-     * uid
-     * 1: object
-     * table
-     * t3ver_oid
-     * uid
-     * nextStage
-     * recipients: array with uids
-     * additional: string
-     * comments: string
-     *
-     * @param \stdClass $parameters
-     * @return array
-     */
-    public function sendToSpecificStageExecute(\stdClass $parameters)
-    {
-        $cmdArray = [];
-        $setStageId = $parameters->affects->nextStage;
-        $comments = $parameters->comments;
-        $elements = $parameters->affects->elements;
-        $recipients = $this->getRecipientList((array)$parameters->recipients, $parameters->additional, $setStageId);
-        foreach ($elements as $element) {
-            // Avoid any action on records that have already been published to live
-            $elementRecord = BackendUtility::getRecord($element->table, $element->uid);
-            if ((int)$elementRecord['t3ver_wsid'] === 0) {
-                continue;
-            }
-
-            if ($setStageId == StagesService::STAGE_PUBLISH_EXECUTE_ID) {
-                $cmdArray[$element->table][$element->t3ver_oid]['version']['action'] = 'swap';
-                $cmdArray[$element->table][$element->t3ver_oid]['version']['swapWith'] = $element->uid;
-                $cmdArray[$element->table][$element->t3ver_oid]['version']['comment'] = $comments;
-                $cmdArray[$element->table][$element->t3ver_oid]['version']['notificationAlternativeRecipients'] = $recipients;
-            } else {
-                $cmdArray[$element->table][$element->uid]['version']['action'] = 'setStage';
-                $cmdArray[$element->table][$element->uid]['version']['stageId'] = $setStageId;
-                $cmdArray[$element->table][$element->uid]['version']['comment'] = $comments;
-                $cmdArray[$element->table][$element->uid]['version']['notificationAlternativeRecipients'] = $recipients;
-            }
-        }
-        $this->processTcaCmd($cmdArray);
-        $result = [
-            'success' => true
-        ];
-        return $result;
-    }
-
-    /**
-     * Gets the dialog window to be displayed before a record can be sent to a stage.
-     *
-     * @param StageRecord|int $nextStage
-     * @return array
-     */
-    protected function getSentToStageWindow($nextStage)
-    {
-        if (!$nextStage instanceof StageRecord) {
-            $nextStage = WorkspaceRecord::get($this->getCurrentWorkspace())->getStage($nextStage);
-        }
-
-        $result = [];
-        if ($nextStage->isDialogEnabled()) {
-            $result['sendMailTo'] = $this->getRecipientsOfStage($nextStage->getUid());
-            $result['additional'] = [
-                'type' => 'textarea',
-                'value' => ''
-            ];
-        }
-        $result['comments'] = [
-            'type' => 'textarea',
-            'value' => ($nextStage->isInternal() ? '' : $nextStage->getDefaultComment())
-        ];
-
-        return $result;
-    }
-
-    /**
-     * Gets all assigned recipients of a particular stage.
-     *
-     * @param StageRecord|int $stageRecord
-     * @return array
-     */
-    protected function getRecipientsOfStage($stageRecord)
-    {
-        if (!$stageRecord instanceof StageRecord) {
-            $stageRecord = WorkspaceRecord::get($this->getCurrentWorkspace())->getStage($stageRecord);
-        }
-
-        $result = [];
-        $allRecipients = $this->getStageService()->getResponsibleBeUser($stageRecord);
-        $preselectedRecipients = $this->stageService->getPreselectedRecipients($stageRecord);
-        $isPreselectionChangeable = $stageRecord->isPreselectionChangeable();
-
-        foreach ($allRecipients as $backendUserId => $backendUser) {
-            if (empty($backendUser['email']) || !GeneralUtility::validEmail($backendUser['email'])) {
-                continue;
-            }
-
-            $name = (!empty($backendUser['realName']) ? $backendUser['realName'] : $backendUser['username']);
-            $checked = in_array($backendUserId, $preselectedRecipients);
-            $disabled = ($checked && !$isPreselectionChangeable);
-
-            $result[] = [
-                'label' => sprintf('%s (%s)', $name, $backendUser['email']),
-                'value' => $backendUserId,
-                'name' => 'recipients-' . $backendUserId,
-                'checked' => $checked,
-                'disabled' => $disabled
-            ];
-        }
-
-        return $result;
-    }
-
-    /**
-     * Gets the default comment of a particular stage.
-     *
-     * @param int $stage
-     * @return string
-     */
-    protected function getDefaultCommentOfStage($stage)
-    {
-        $result = $this->getStageService()->getPropertyOfCurrentWorkspaceStage($stage, 'default_mailcomment');
-        return $result;
-    }
-
-    /**
-     * Gets an instance of the Stage service.
-     *
-     * @return StagesService
-     */
-    protected function getStageService()
-    {
-        if (!isset($this->stageService)) {
-            $this->stageService = GeneralUtility::makeInstance(StagesService::class);
-        }
-        return $this->stageService;
-    }
-
-    /**
-     * Send all available workspace records to the previous stage.
-     *
-     * @param int $id Current page id to process items to previous stage.
-     * @return array
-     */
-    public function sendPageToPreviousStage($id)
-    {
-        $workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
-        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($this->stageService->getWorkspaceId(), ($filter = 1), ($stage = -99), $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
-        list($currentStage, $previousStage) = $this->getStageService()->getPreviousStageForElementCollection($workspaceItemsArray);
-        // get only the relevant items for processing
-        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($this->stageService->getWorkspaceId(), ($filter = 1), $currentStage['uid'], $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
-        $stageFormFields = $this->getSentToStageWindow($previousStage['uid']);
-        $result = array_merge($stageFormFields, [
-            'title' => 'Status message: Page send to next stage - ID: ' . $id . ' - Next stage title: ' . $previousStage['title'],
-            'items' => $this->getSentToStageWindow($previousStage['uid']),
-            'affects' => $workspaceItemsArray,
-            'stageId' => $previousStage['uid']
-        ]);
-        return $result;
-    }
-
-    /**
-     * @param int $id Current Page id to select Workspace items from.
-     * @return array
-     */
-    public function sendPageToNextStage($id)
-    {
-        $workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
-        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($this->stageService->getWorkspaceId(), ($filter = 1), ($stage = -99), $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
-        list($currentStage, $nextStage) = $this->getStageService()->getNextStageForElementCollection($workspaceItemsArray);
-        // get only the relevant items for processing
-        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($this->stageService->getWorkspaceId(), ($filter = 1), $currentStage['uid'], $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
-        $stageFormFields = $this->getSentToStageWindow($nextStage['uid']);
-        $result = array_merge($stageFormFields, [
-            'title' => 'Status message: Page send to next stage - ID: ' . $id . ' - Next stage title: ' . $nextStage['title'],
-            'affects' => $workspaceItemsArray,
-            'stageId' => $nextStage['uid']
-        ]);
-        return $result;
-    }
-
-    /**
-     * Fetch the current label and visible state of the buttons.
-     *
-     * @param int $id
-     * @return string The pre-rendered HTML for the stage buttons
-     */
-    public function updateStageChangeButtons($id)
-    {
-        /** @var StagesService $stageService */
-        $stageService = GeneralUtility::makeInstance(StagesService::class);
-        /** @var WorkspaceService $workspaceService */
-        $workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
-        // fetch the next and previous stage
-        $workspaceItemsArray = $workspaceService->selectVersionsInWorkspace($stageService->getWorkspaceId(), ($filter = 1), ($stage = -99), $id, ($recursionLevel = 0), ($selectionType = 'tables_modify'));
-        list(, $nextStage) = $stageService->getNextStageForElementCollection($workspaceItemsArray);
-        list(, $previousStage) = $stageService->getPreviousStageForElementCollection($workspaceItemsArray);
-
-        /** @var StandaloneView $view */
-        $view = GeneralUtility::makeInstance(StandaloneView::class);
-        $extensionPath = ExtensionManagementUtility::extPath('workspaces');
-        $view->setPartialRootPaths(['default' => $extensionPath . 'Resources/Private/Partials']);
-        $view->setTemplatePathAndFilename($extensionPath . 'Resources/Private/Templates/Preview/Ajax/StageButtons.html');
-        $request = $view->getRequest();
-        $request->setControllerExtensionName('workspaces');
-        $view->assignMultiple([
-            'enablePreviousStageButton' => is_array($previousStage) && !empty($previousStage),
-            'enableNextStageButton' => is_array($nextStage) && !empty($nextStage),
-            'enableDiscardStageButton' => is_array($nextStage) && !empty($nextStage) || is_array($previousStage) && !empty($previousStage),
-            'nextStage' => $nextStage['title'],
-            'nextStageId' => $nextStage['uid'],
-            'prevStage' => $previousStage['title'],
-            'prevStageId' => $previousStage['uid'],
-        ]);
-        $renderedView = $view->render();
-        return $renderedView;
-    }
-
-    /**
-     * @param int $workspaceId
-     * @return int Id of the original workspace
-     * @throws \TYPO3\CMS\Core\Exception
-     */
-    protected function setTemporaryWorkspace($workspaceId)
-    {
-        $workspaceId = (int)$workspaceId;
-        $currentWorkspace = (int)$this->getBackendUser()->workspace;
-
-        if ($currentWorkspace !== $workspaceId) {
-            if (!$this->getBackendUser()->setTemporaryWorkspace($workspaceId)) {
-                throw new \TYPO3\CMS\Core\Exception(
-                    'Cannot set temporary workspace to "' . $workspaceId . '"',
-                    1371484524
-                );
-            }
-        }
-
-        return $currentWorkspace;
-    }
-
-    /**
-     * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
-     */
-    protected function getBackendUser()
-    {
-        return $GLOBALS['BE_USER'];
-    }
-}
diff --git a/typo3/sysext/workspaces/Classes/ExtDirect/ExtDirectServer.php b/typo3/sysext/workspaces/Classes/ExtDirect/ExtDirectServer.php
deleted file mode 100644 (file)
index 6b9b5bd..0000000
+++ /dev/null
@@ -1,516 +0,0 @@
-<?php
-namespace TYPO3\CMS\Workspaces\ExtDirect;
-
-/*
- * 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\Backend\Avatar\Avatar;
-use TYPO3\CMS\Backend\Utility\BackendUtility;
-use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
-use TYPO3\CMS\Core\Database\ConnectionPool;
-use TYPO3\CMS\Core\Html\RteHtmlParser;
-use TYPO3\CMS\Core\Imaging\Icon;
-use TYPO3\CMS\Core\Imaging\IconFactory;
-use TYPO3\CMS\Core\Resource\FileReference;
-use TYPO3\CMS\Core\Resource\ProcessedFile;
-use TYPO3\CMS\Core\Utility\DiffUtility;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Core\Utility\MathUtility;
-use TYPO3\CMS\Extbase\Object\ObjectManager;
-use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
-use TYPO3\CMS\Lang\LanguageService;
-use TYPO3\CMS\Workspaces\Service\GridDataService;
-use TYPO3\CMS\Workspaces\Service\HistoryService;
-use TYPO3\CMS\Workspaces\Service\StagesService;
-use TYPO3\CMS\Workspaces\Service\WorkspaceService;
-
-/**
- * ExtDirect server
- */
-class ExtDirectServer extends AbstractHandler
-{
-    /**
-     * @var GridDataService
-     */
-    protected $gridDataService;
-
-    /**
-     * @var StagesService
-     */
-    protected $stagesService;
-
-    /**
-     * @var DiffUtility
-     */
-    protected $differenceHandler;
-
-    /**
-     * Checks integrity of elements before peforming actions on them.
-     *
-     * @param \stdClass $parameters
-     * @return array
-     */
-    public function checkIntegrity(\stdClass $parameters)
-    {
-        $integrity = $this->createIntegrityService($this->getAffectedElements($parameters));
-        $integrity->check();
-        $response = [
-            'result' => $integrity->getStatusRepresentation()
-        ];
-        return $response;
-    }
-
-    /**
-     * Get List of workspace changes
-     *
-     * @param \stdClass $parameter
-     * @return array $data
-     */
-    public function getWorkspaceInfos($parameter)
-    {
-        // To avoid too much work we use -1 to indicate that every page is relevant
-        $pageId = $parameter->id > 0 ? $parameter->id : -1;
-        if (!isset($parameter->language) || !MathUtility::canBeInterpretedAsInteger($parameter->language)) {
-            $parameter->language = null;
-        }
-        $versions = $this->getWorkspaceService()->selectVersionsInWorkspace($this->getCurrentWorkspace(), 0, -99, $pageId, $parameter->depth, 'tables_select', $parameter->language);
-        $data = $this->getGridDataService()->generateGridListFromVersions($versions, $parameter, $this->getCurrentWorkspace());
-        return $data;
-    }
-
-    /**
-     * Get List of available workspace actions
-     *
-     * @param \stdClass $parameter
-     * @return array $data
-     */
-    public function getStageActions(\stdClass $parameter)
-    {
-        $currentWorkspace = $this->getCurrentWorkspace();
-        $stages = [];
-        if ($currentWorkspace != WorkspaceService::SELECT_ALL_WORKSPACES) {
-            $stages = $this->getStagesService()->getStagesForWSUser();
-        }
-        $data = [
-            'total' => count($stages),
-            'data' => $stages
-        ];
-        return $data;
-    }
-
-    /**
-     * Fetch further information to current selected workspace record.
-     *
-     * @param \stdClass $parameter
-     * @return array $data
-     */
-    public function getRowDetails($parameter)
-    {
-        $diffReturnArray = [];
-        $liveReturnArray = [];
-        $diffUtility = $this->getDifferenceHandler();
-        /** @var $parseObj RteHtmlParser */
-        $parseObj = GeneralUtility::makeInstance(RteHtmlParser::class);
-        $liveRecord = BackendUtility::getRecord($parameter->table, $parameter->t3ver_oid);
-        $versionRecord = BackendUtility::getRecord($parameter->table, $parameter->uid);
-        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        $icon_Live = $iconFactory->getIconForRecord($parameter->table, $liveRecord, Icon::SIZE_SMALL)->render();
-        $icon_Workspace = $iconFactory->getIconForRecord($parameter->table, $versionRecord, Icon::SIZE_SMALL)->render();
-        $stagesService = $this->getStagesService();
-        $stagePosition = $stagesService->getPositionOfCurrentStage($parameter->stage);
-        $fieldsOfRecords = array_keys($liveRecord);
-        if ($GLOBALS['TCA'][$parameter->table]) {
-            if ($GLOBALS['TCA'][$parameter->table]['interface']['showRecordFieldList']) {
-                $fieldsOfRecords = $GLOBALS['TCA'][$parameter->table]['interface']['showRecordFieldList'];
-                $fieldsOfRecords = GeneralUtility::trimExplode(',', $fieldsOfRecords, true);
-            }
-        }
-        foreach ($fieldsOfRecords as $fieldName) {
-            if (empty($GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'])) {
-                continue;
-            }
-            // Get the field's label. If not available, use the field name
-            $fieldTitle = $this->getLanguageService()->sL(BackendUtility::getItemLabel($parameter->table, $fieldName));
-            if (empty($fieldTitle)) {
-                $fieldTitle = $fieldName;
-            }
-            // Gets the TCA configuration for the current field
-            $configuration = $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['config'];
-            // check for exclude fields
-            if ($this->getBackendUser()->isAdmin() || $GLOBALS['TCA'][$parameter->table]['columns'][$fieldName]['exclude'] == 0 || GeneralUtility::inList($this->getBackendUser()->groupData['non_exclude_fields'], $parameter->table . ':' . $fieldName)) {
-                // call diff class only if there is a difference
-                if ($configuration['type'] === 'inline' && $configuration['foreign_table'] === 'sys_file_reference') {
-                    $useThumbnails = false;
-                    if (!empty($configuration['foreign_selector_fieldTcaOverride']['config']['appearance']['elementBrowserAllowed']) && !empty($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'])) {
-                        $fileExtensions = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], true);
-                        $allowedExtensions = GeneralUtility::trimExplode(',', $configuration['foreign_selector_fieldTcaOverride']['config']['appearance']['elementBrowserAllowed'], true);
-                        $differentExtensions = array_diff($allowedExtensions, $fileExtensions);
-                        $useThumbnails = empty($differentExtensions);
-                    }
-
-                    $liveFileReferences = BackendUtility::resolveFileReferences(
-                        $parameter->table,
-                        $fieldName,
-                        $liveRecord,
-                        0
-                    );
-                    $versionFileReferences = BackendUtility::resolveFileReferences(
-                        $parameter->table,
-                        $fieldName,
-                        $versionRecord,
-                        $this->getCurrentWorkspace()
-                    );
-                    $fileReferenceDifferences = $this->prepareFileReferenceDifferences(
-                        $liveFileReferences,
-                        $versionFileReferences,
-                        $useThumbnails
-                    );
-
-                    if ($fileReferenceDifferences === null) {
-                        continue;
-                    }
-
-                    $diffReturnArray[] = [
-                        'field' => $fieldName,
-                        'label' => $fieldTitle,
-                        'content' => $fileReferenceDifferences['differences']
-                    ];
-                    $liveReturnArray[] = [
-                        'field' => $fieldName,
-                        'label' => $fieldTitle,
-                        'content' => $fileReferenceDifferences['live']
-                    ];
-                } elseif ((string)$liveRecord[$fieldName] !== (string)$versionRecord[$fieldName]) {
-                    // Select the human readable values before diff
-                    $liveRecord[$fieldName] = BackendUtility::getProcessedValue(
-                        $parameter->table,
-                        $fieldName,
-                        $liveRecord[$fieldName],
-                        0,
-                        1,
-                        false,
-                        $liveRecord['uid']
-                    );
-                    $versionRecord[$fieldName] = BackendUtility::getProcessedValue(
-                        $parameter->table,
-                        $fieldName,
-                        $versionRecord[$fieldName],
-                        0,
-                        1,
-                        false,
-                        $versionRecord['uid']
-                    );
-
-                    if ($configuration['type'] == 'group' && $configuration['internal_type'] == 'file') {
-                        $versionThumb = BackendUtility::thumbCode($versionRecord, $parameter->table, $fieldName, '');
-                        $liveThumb = BackendUtility::thumbCode($liveRecord, $parameter->table, $fieldName, '');
-                        $diffReturnArray[] = [
-                            'field' => $fieldName,
-                            'label' => $fieldTitle,
-                            'content' => $versionThumb
-                        ];
-                        $liveReturnArray[] = [
-                            'field' => $fieldName,
-                            'label' => $fieldTitle,
-                            'content' => $liveThumb
-                        ];
-                    } else {
-                        $diffReturnArray[] = [
-                            'field' => $fieldName,
-                            'label' => $fieldTitle,
-                            'content' => $diffUtility->makeDiffDisplay($liveRecord[$fieldName], $versionRecord[$fieldName])
-                        ];
-                        $liveReturnArray[] = [
-                            'field' => $fieldName,
-                            'label' => $fieldTitle,
-                            'content' => $parseObj->TS_images_rte($liveRecord[$fieldName])
-                        ];
-                    }
-                }
-            }
-        }
-        // Hook for modifying the difference and live arrays
-        // (this may be used by custom or dynamically-defined fields)
-        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'])) {
-            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['workspaces']['modifyDifferenceArray'] as $className) {
-                $hookObject = GeneralUtility::getUserObj($className);
-                if (method_exists($hookObject, 'modifyDifferenceArray')) {
-                    $hookObject->modifyDifferenceArray($parameter, $diffReturnArray, $liveReturnArray, $diffUtility);
-                }
-            }
-        }
-        $commentsForRecord = $this->getCommentsForRecord($parameter->uid, $parameter->table);
-
-        /** @var $historyService HistoryService */
-        $historyService = GeneralUtility::makeInstance(HistoryService::class);
-        $history = $historyService->getHistory($parameter->table, $parameter->t3ver_oid);
-
-        $prevStage = $stagesService->getPrevStage($parameter->stage);
-        $nextStage = $stagesService->getNextStage($parameter->stage);
-
-        if (isset($prevStage[0])) {
-            $prevStage = current($prevStage);
-        }
-
-        if (isset($nextStage[0])) {
-            $nextStage = current($nextStage);
-        }
-
-        return [
-            'total' => 1,
-            'data' => [
-                [
-                    // these parts contain HTML (don't escape)
-                    'diff' => $diffReturnArray,
-                    'live_record' => $liveReturnArray,
-                    'icon_Live' => $icon_Live,
-                    'icon_Workspace' => $icon_Workspace,
-                    // this part is already escaped in getCommentsForRecord()
-                    'comments' => $commentsForRecord,
-                    // escape/sanitize the others
-                    'path_Live' => htmlspecialchars(BackendUtility::getRecordPath($liveRecord['pid'], '', 999)),
-                    'label_Stage' => htmlspecialchars($stagesService->getStageTitle($parameter->stage)),
-                    'label_PrevStage' => $prevStage,
-                    'label_NextStage' => $nextStage,
-                    'stage_position' => (int)$stagePosition['position'],
-                    'stage_count' => (int)$stagePosition['count'],
-                    'parent' => [
-                        'table' => htmlspecialchars($parameter->table),
-                        'uid' => (int)$parameter->uid
-                    ],
-                    'history' => [
-                        'data' => $history,
-                        'total' => count($history)
-                    ]
-                ]
-            ]
-        ];
-    }
-
-    /**
-     * Prepares difference view for file references.
-     *
-     * @param FileReference[] $liveFileReferences
-     * @param FileReference[] $versionFileReferences
-     * @param bool|false $useThumbnails
-     * @return array|null
-     */
-    protected function prepareFileReferenceDifferences(array $liveFileReferences, array $versionFileReferences, $useThumbnails = false)
-    {
-        $randomValue = uniqid('file');
-
-        $liveValues = [];
-        $versionValues = [];
-        $candidates = [];
-        $substitutes = [];
-
-        // Process live references
-        foreach ($liveFileReferences as $identifier => $liveFileReference) {
-            $identifierWithRandomValue = $randomValue . '__' . $liveFileReference->getUid() . '__' . $randomValue;
-            $candidates[$identifierWithRandomValue] = $liveFileReference;
-            $liveValues[] = $identifierWithRandomValue;
-        }
-
-        // Process version references
-        foreach ($versionFileReferences as $identifier => $versionFileReference) {
-            $identifierWithRandomValue = $randomValue . '__' . $versionFileReference->getUid() . '__' . $randomValue;
-            $candidates[$identifierWithRandomValue] = $versionFileReference;
-            $versionValues[] = $identifierWithRandomValue;
-        }
-
-        // Combine values and surround by spaces
-        // (to reduce the chunks Diff will find)
-        $liveInformation = ' ' . implode(' ', $liveValues) . ' ';
-        $versionInformation = ' ' . implode(' ', $versionValues) . ' ';
-
-        // Return if information has not changed
-        if ($liveInformation === $versionInformation) {
-            return null;
-        }
-
-        /**
-         * @var string $identifierWithRandomValue
-         * @var FileReference $fileReference
-         */
-        foreach ($candidates as $identifierWithRandomValue => $fileReference) {
-            if ($useThumbnails) {
-                $thumbnailFile = $fileReference->getOriginalFile()->process(
-                    ProcessedFile::CONTEXT_IMAGEPREVIEW,
-                    ['width' => 40, 'height' => 40]
-                );
-                $thumbnailMarkup = '<img src="' . $thumbnailFile->getPublicUrl(true) . '" />';
-                $substitutes[$identifierWithRandomValue] = $thumbnailMarkup;
-            } else {
-                $substitutes[$identifierWithRandomValue] = $fileReference->getPublicUrl();
-            }
-        }
-
-        $differences = $this->getDifferenceHandler()->makeDiffDisplay($liveInformation, $versionInformation);
-        $liveInformation = str_replace(array_keys($substitutes), array_values($substitutes), trim($liveInformation));
-        $differences = str_replace(array_keys($substitutes), array_values($substitutes), trim($differences));
-
-        return [
-            'live' => $liveInformation,
-            'differences' => $differences
-        ];
-    }
-
-    /**
-     * Gets an array with all sys_log entries and their comments for the given record uid and table
-     *
-     * @param int $uid uid of changed element to search for in log
-     * @param string $table Name of the record's table
-     * @return array
-     */
-    public function getCommentsForRecord($uid, $table)
-    {
-        $sysLogReturnArray = [];
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_log');
-
-        $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($table, \PDO::PARAM_STR)
-                ),
-                $queryBuilder->expr()->eq(
-                    'recuid',
-                    $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
-                )
-            )
-            ->orderBy('tstamp', 'DESC')
-            ->execute();
-
-        /** @var Avatar $avatar */
-        $avatar = GeneralUtility::makeInstance(Avatar::class);
-
-        while ($sysLogRow = $result->fetch()) {
-            $sysLogEntry = [];
-            $data = unserialize($sysLogRow['log_data']);
-            $beUserRecord = BackendUtility::getRecord('be_users', $sysLogRow['userid']);
-            $sysLogEntry['stage_title'] = htmlspecialchars($this->getStagesService()->getStageTitle($data['stage']));
-            $sysLogEntry['user_uid'] = (int)$sysLogRow['userid'];
-            $sysLogEntry['user_username'] = is_array($beUserRecord) ? htmlspecialchars($beUserRecord['username']) : '';
-            $sysLogEntry['tstamp'] = htmlspecialchars(BackendUtility::datetime($sysLogRow['tstamp']));
-            $sysLogEntry['user_comment'] = nl2br(htmlspecialchars($data['comment']));
-            $sysLogEntry['user_avatar'] = $avatar->render($beUserRecord);
-            $sysLogReturnArray[] = $sysLogEntry;
-        }
-        return $sysLogReturnArray;
-    }
-
-    /**
-     * Gets all available system languages.
-     *
-     * @return array
-     */
-    public function getSystemLanguages()
-    {
-        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
-        $systemLanguages = [
-            [
-                'uid' => 'all',
-                'title' => LocalizationUtility::translate('language.allLanguages', 'workspaces'),
-                'icon' => $iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render()
-            ]
-        ];
-        foreach ($this->getGridDataService()->getSystemLanguages() as $id => $systemLanguage) {
-            if ($id < 0) {
-                continue;
-            }
-            $systemLanguages[] = [
-                'uid' => $id,
-                'title' => htmlspecialchars($systemLanguage['title']),
-                'icon' => $iconFactory->getIcon($systemLanguage['flagIcon'], Icon::SIZE_SMALL)->render()
-            ];
-        }
-        $result = [
-            'total' => count($systemLanguages),
-            'data' => $systemLanguages
-        ];
-        return $result;
-    }
-
-    /**
-     * @return BackendUserAuthentication;
-     */
-    protected function getBackendUser()
-    {
-        return $GLOBALS['BE_USER'];
-    }
-
-    /**
-     * @return LanguageService;
-     */
-    protected function getLanguageService()
-    {
-        return $GLOBALS['LANG'];
-    }
-
-    /**
-     * Gets the Grid Data Service.
-     *
-     * @return GridDataService
-     */
-    protected function getGridDataService()
-    {
-        if (!isset($this->gridDataService)) {
-            $this->gridDataService = GeneralUtility::makeInstance(GridDataService::class);
-        }
-        return $this->gridDataService;
-    }
-
-    /**
-     * Gets the Stages Service.
-     *
-     * @return StagesService
-     */
-    protected function getStagesService()
-    {
-        if (!isset($this->stagesService)) {
-            $this->stagesService = GeneralUtility::makeInstance(StagesService::class);
-        }
-        return $this->stagesService;
-    }
-
-    /**
-     * Gets the difference handler, parsing differences based on sentences.
-     *
-     * @return DiffUtility
-     */
-    protected function getDifferenceHandler()
-    {
-        if (!isset($this->differenceHandler)) {
-            $this->differenceHandler = GeneralUtility::makeInstance(DiffUtility::class);
-        }
-        return $this->differenceHandler;
-    }
-
-    /**
-     * @return \TYPO3\CMS\Extbase\Object\ObjectManager
-     */
-    protected function getObjectManager()
-    {
-        return GeneralUtility::makeInstance(ObjectManager::class);
-    }
-}
diff --git a/typo3/sysext/workspaces/Classes/ExtDirect/MassActionHandler.php b/typo3/sysext/workspaces/Classes/ExtDirect/MassActionHandler.php
deleted file mode 100644 (file)
index 7b14447..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-<?php
-namespace TYPO3\CMS\Workspaces\ExtDirect;
-
-/*
- * 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!
- */
-
-/**
- * Class encapsulates all actions which are triggered for all elements within the current workspace.
- */
-class MassActionHandler extends AbstractHandler
-{
-    const MAX_RECORDS_TO_PROCESS = 30;
-
-    /**
-     * Path to the locallang file
-     *
-     * @var string
-     */
-    private $pathToLocallang = 'LLL:EXT:workspaces/Resources/Private/Language/locallang.xlf';
-
-    /**
-     * Get list of available mass workspace actions.
-     *
-     * @param \stdClass $parameter
-     * @return array $data
-     */
-    public function getMassStageActions($parameter)
-    {
-        $actions = [];
-        $currentWorkspace = $this->getCurrentWorkspace();
-        $massActionsEnabled = $GLOBALS['BE_USER']->getTSConfigVal('options.workspaces.enableMassActions') !== '0';
-        // in case we're working within "All Workspaces" we can't provide Mass Actions
-        if ($currentWorkspace != \TYPO3\CMS\Workspaces\Service\WorkspaceService::SELECT_ALL_WORKSPACES && $massActionsEnabled) {
-            $publishAccess = $GLOBALS['BE_USER']->workspacePublishAccess($currentWorkspace);
-            if ($publishAccess && !($GLOBALS['BE_USER']->workspaceRec['publish_access'] & 1)) {
-                $actions[] = ['action' => 'publish', 'title' => $GLOBALS['LANG']->sL($this->pathToLocallang . ':label_doaction_publish')];
-                if ($GLOBALS['BE_USER']->workspaceSwapAccess()) {
-                    $actions[] = ['action' => 'swap', 'title' => $GLOBALS['LANG']->sL($this->pathToLocallang . ':label_doaction_swap')];
-                }
-            }
-            if ($currentWorkspace !== \TYPO3\CMS\Workspaces\Service\WorkspaceService::LIVE_WORKSPACE_ID) {
-                $actions[] = ['action' => 'discard', 'title' => $GLOBALS['LANG']->sL($this->pathToLocallang . ':label_doaction_discard')];
-            }
-        }
-        $result = [
-            'total' => count($actions),
-            'data' => $actions
-        ];
-        return $result;
-    }
-
-    /**
-     * Publishes the current workspace.
-     *
-     * @param \stdClass $parameters
-     * @return array
-     */
-    public function publishWorkspace(\stdClass $parameters)
-    {
-        $result = [
-            'init' => false,
-            'total' => 0,
-            'processed' => 0,
-            'error' => false
-        ];
-        try {
-            if ($parameters->init) {
-                $language = $this->validateLanguageParameter($parameters);
-                $cnt = $this->initPublishData($this->getCurrentWorkspace(), $parameters->swap, $language);
-                $result['total'] = $cnt;
-            } else {
-                $result['processed'] = $this->processData($this->getCurrentWorkspace());
-                $result['total'] = $GLOBALS['BE_USER']->getSessionData('workspaceMassAction_total');
-            }
-        } catch (\Exception $e) {
-            $result['error'] = $e->getMessage();
-        }
-        return $result;
-    }
-
-    /**
-     * Flushes the current workspace.
-     *
-     * @param \stdClass $parameters
-     * @return array
-     */
-    public function flushWorkspace(\stdClass $parameters)
-    {
-        $result = [
-            'init' => false,
-            'total' => 0,
-            'processed' => 0,
-            'error' => false
-        ];
-        try {
-            if ($parameters->init) {
-                $language = $this->validateLanguageParameter($parameters);
-                $cnt = $this->initFlushData($this->getCurrentWorkspace(), $language);
-                $result['total'] = $cnt;
-            } else {
-                $result['processed'] = $this->processData($this->getCurrentWorkspace());
-                $result['total'] = $GLOBALS['BE_USER']->getSessionData('workspaceMassAction_total');
-            }
-        } catch (\Exception $e) {
-            $result['error'] = $e->getMessage();
-        }
-        return $result;
-    }
-
-    /**
-     * Initializes the command map to be used for publishing.
-     *
-     * @param int $workspace
-     * @param bool $swap
-     * @param int $language
-     * @return int
-     */
-    protected function initPublishData($workspace, $swap, $language = null)
-    {
-        // workspace might be -98 a.k.a "All Workspaces but that's save here
-        $publishData = $this->getWorkspaceService()->getCmdArrayForPublishWS($workspace, $swap, 0, $language);
-        $recordCount = 0;
-        foreach ($publishData as $table => $recs) {
-            $recordCount += count($recs);
-        }
-        if ($recordCount > 0) {
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction', $publishData);
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_total', $recordCount);
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_processed', 0);
-        }
-        return $recordCount;
-    }
-
-    /**
-     * Initializes the command map to be used for flushing.
-     *
-     * @param int $workspace
-     * @param int $language
-     * @return int
-     */
-    protected function initFlushData($workspace, $language = null)
-    {
-        // workspace might be -98 a.k.a "All Workspaces but that's save here
-        $flushData = $this->getWorkspaceService()->getCmdArrayForFlushWS($workspace, true, 0, $language);
-        $recordCount = 0;
-        foreach ($flushData as $table => $recs) {
-            $recordCount += count($recs);
-        }
-        if ($recordCount > 0) {
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction', $flushData);
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_total', $recordCount);
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_processed', 0);
-        }
-        return $recordCount;
-    }
-
-    /**
-     * Processes the data.
-     *
-     * @param int $workspace
-     * @return int
-     */
-    protected function processData($workspace)
-    {
-        $processData = $GLOBALS['BE_USER']->getSessionData('workspaceMassAction');
-        $recordsProcessed = $GLOBALS['BE_USER']->getSessionData('workspaceMassAction_processed');
-        $limitedCmd = [];
-        $numRecs = 0;
-        foreach ($processData as $table => $recs) {
-            foreach ($recs as $key => $value) {
-                $numRecs++;
-                $limitedCmd[$table][$key] = $value;
-                if ($numRecs == self::MAX_RECORDS_TO_PROCESS) {
-                    break;
-                }
-            }
-            if ($numRecs == self::MAX_RECORDS_TO_PROCESS) {
-                break;
-            }
-        }
-        if ($numRecs == 0) {
-            // All done
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction', null);
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_total', 0);
-        } else {
-            /** @var $tce \TYPO3\CMS\Core\DataHandling\DataHandler */
-            $tce = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
-            // Execute the commands:
-            $tce->start([], $limitedCmd);
-            $tce->process_cmdmap();
-            $errors = $tce->errorLog;
-            if (!empty($errors)) {
-                throw new \Exception(implode(', ', $errors), 1476048278);
-            }
-            // Unset processed records
-            foreach ($limitedCmd as $table => $recs) {
-                foreach ($recs as $key => $value) {
-                    $recordsProcessed++;
-                    unset($processData[$table][$key]);
-                }
-            }
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction', $processData);
-            $GLOBALS['BE_USER']->setAndSaveSessionData('workspaceMassAction_processed', $recordsProcessed);
-        }
-        return $recordsProcessed;
-    }
-}
diff --git a/typo3/sysext/workspaces/Classes/ExtDirect/PagetreeCollectionsProcessor.php b/typo3/sysext/workspaces/Classes/ExtDirect/PagetreeCollectionsProcessor.php
deleted file mode 100644 (file)
index a5323b5..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-<?php
-namespace TYPO3\CMS\Workspaces\ExtDirect;
-
-/*
- * 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\Utility\GeneralUtility;
-use TYPO3\CMS\Workspaces\Service\WorkspaceService;
-
-/**
- * Interface for classes which perform pre or post processing
- */
-class PagetreeCollectionsProcessor implements \TYPO3\CMS\Backend\Tree\Pagetree\CollectionProcessorInterface
-{
-    /**
-     * @var WorkspaceService
-     */
-    protected $workspaceService = null;
-
-    /**
-     * @abstract
-     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode $node
-     * @param int $mountPoint
-     * @param int $level
-     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection $nodeCollection
-     * @return void
-     */
-    public function postProcessGetNodes($node, $mountPoint, $level, $nodeCollection)
-    {
-        foreach ($nodeCollection as $node) {
-            /** @var $node \TYPO3\CMS\Backend\Tree\TreeNode */
-            $this->highlightVersionizedElements($node);
-        }
-    }
-
-    /**
-     * @abstract
-     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode $node
-     * @param string $searchFilter
-     * @param int $mountPoint
-     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection $nodeCollection
-     * @return void
-     */
-    public function postProcessFilteredNodes($node, $searchFilter, $mountPoint, $nodeCollection)
-    {
-        foreach ($nodeCollection as $node) {
-            /** @var $node \TYPO3\CMS\Backend\Tree\TreeNode */
-            $this->highlightVersionizedElements($node);
-        }
-    }
-
-    /**
-     * @abstract
-     * @param string $searchFilter
-     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection $nodeCollection
-     * @return void
-     */
-    public function postProcessGetTreeMounts($searchFilter, $nodeCollection)
-    {
-        foreach ($nodeCollection as $node) {
-            /** @var $node \TYPO3\CMS\Backend\Tree\TreeNode */
-            $this->highlightVersionizedElements($node);
-        }
-    }
-
-    /**
-     * Sets the CSS Class on all pages which have versioned records
-     * in the current workspace
-     *
-     * @param \TYPO3\CMS\Backend\Tree\TreeNode $node
-     * @return void
-     */
-    protected function highlightVersionizedElements(\TYPO3\CMS\Backend\Tree\TreeNode $node)
-    {
-        if (!$node->getCls() && $this->getWorkspaceService()->hasPageRecordVersions($GLOBALS['BE_USER']->workspace, $node->getId())) {
-            $node->setCls('ver-versions');
-        }
-    }
-
-    /**
-     * @return WorkspaceService
-     */
-    protected function getWorkspaceService()
-    {
-        if ($this->workspaceService === null) {
-            $this->workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
-        }
-
-        return $this->workspaceService;
-    }
-}
diff --git a/typo3/sysext/workspaces/Classes/Hooks/PagetreeCollectionsProcessor.php b/typo3/sysext/workspaces/Classes/Hooks/PagetreeCollectionsProcessor.php
new file mode 100644 (file)
index 0000000..da58716
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Hooks;
+
+/*
+ * 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\Utility\GeneralUtility;
+use TYPO3\CMS\Workspaces\Service\WorkspaceService;
+
+/**
+ * Interface for classes which perform pre or post processing
+ */
+class PagetreeCollectionsProcessor implements \TYPO3\CMS\Backend\Tree\Pagetree\CollectionProcessorInterface
+{
+    /**
+     * @var WorkspaceService
+     */
+    protected $workspaceService = null;
+
+    /**
+     * @abstract
+     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode $node
+     * @param int $mountPoint
+     * @param int $level
+     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection $nodeCollection
+     * @return void
+     */
+    public function postProcessGetNodes($node, $mountPoint, $level, $nodeCollection)
+    {
+        foreach ($nodeCollection as $node) {
+            /** @var $node \TYPO3\CMS\Backend\Tree\TreeNode */
+            $this->highlightVersionizedElements($node);
+        }
+    }
+
+    /**
+     * @abstract
+     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNode $node
+     * @param string $searchFilter
+     * @param int $mountPoint
+     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection $nodeCollection
+     * @return void
+     */
+    public function postProcessFilteredNodes($node, $searchFilter, $mountPoint, $nodeCollection)
+    {
+        foreach ($nodeCollection as $node) {
+            /** @var $node \TYPO3\CMS\Backend\Tree\TreeNode */
+            $this->highlightVersionizedElements($node);
+        }
+    }
+
+    /**
+     * @abstract
+     * @param string $searchFilter
+     * @param \TYPO3\CMS\Backend\Tree\Pagetree\PagetreeNodeCollection $nodeCollection
+     * @return void
+     */
+    public function postProcessGetTreeMounts($searchFilter, $nodeCollection)
+    {
+        foreach ($nodeCollection as $node) {
+            /** @var $node \TYPO3\CMS\Backend\Tree\TreeNode */
+            $this->highlightVersionizedElements($node);
+        }
+    }
+
+    /**
+     * Sets the CSS Class on all pages which have versioned records
+     * in the current workspace
+     *
+     * @param \TYPO3\CMS\Backend\Tree\TreeNode $node
+     * @return void
+     */
+    protected function highlightVersionizedElements(\TYPO3\CMS\Backend\Tree\TreeNode $node)
+    {
+        if (!$node->getCls() && $this->getWorkspaceService()->hasPageRecordVersions($GLOBALS['BE_USER']->workspace, $node->getId())) {
+            $node->setCls('ver-versions');
+        }
+    }
+
+    /**
+     * @return WorkspaceService
+     */
+    protected function getWorkspaceService()
+    {
+        if ($this->workspaceService === null) {
+            $this->workspaceService = GeneralUtility::makeInstance(WorkspaceService::class);
+        }
+
+        return $this->workspaceService;
+    }
+}
index 826bc75..34b770e 100644 (file)
@@ -8,5 +8,9 @@ return [
     'workspace_switch' => [
         'path' => '/workspace/switch',
         'target' => \TYPO3\CMS\Workspaces\Controller\AjaxController::class . '::switchWorkspaceAction'
+    ],
+    'workspace_dispatch' => [
+        'path' => '/workspace/dispatch',
+        'target' => \TYPO3\CMS\Workspaces\Controller\AjaxDispatcher::class . '::dispatch'
     ]
 ];
index 3050b75..5537f25 100644 (file)
@@ -124,8 +124,8 @@ define([
                                );
                                Wizard.addFinalProcessingSlide(function() {
                                        // We passed this slide, swap the record now
-                                       Workspaces.sendExtDirectRequest(
-                                               Workspaces.generateExtDirectActionsPayload('swapSingleRecord', [
+                                       Workspaces.sendRemoteRequest(
+                                               Workspaces.generateRemoteActionsPayload('swapSingleRecord', [
                                                        $tr.data('table'),
                                                        $tr.data('t3ver_oid'),
                                                        $tr.data('uid')
@@ -224,9 +224,9 @@ define([
                        var $me = $(this);
                        Backend.settings.language = $me.val();
 
-                       Workspaces.sendExtDirectRequest([
-                               Workspaces.generateExtDirectActionsPayload('saveLanguageSelection', [$me.val()]),
-                               Workspaces.generateExtDirectPayload('getWorkspaceInfos', Backend.settings)
+                       Workspaces.sendRemoteRequest([
+                               Workspaces.generateRemoteActionsPayload('saveLanguageSelection', [$me.val()]),
+                               Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings)
                        ]).done(function(response) {
                                Backend.elements.$languageSelector.prev().html($me.find(':selected').data('icon'));
                                Backend.renderWorkspaceInfos(response[1].result);
@@ -377,8 +377,8 @@ define([
                        throw 'Invalid direction given.';
                }
 
-               Workspaces.sendExtDirectRequest(
-                       Workspaces.generateExtDirectActionsPayload(stageWindowAction, [
+               Workspaces.sendRemoteRequest(
+                       Workspaces.generateRemoteActionsPayload(stageWindowAction, [
                                $row.data('uid'), $row.data('table'), $row.data('t3ver_oid')
                        ])
                ).done(function(response) {
@@ -396,9 +396,9 @@ define([
                                                elements: []
                                        };
 
-                                       Workspaces.sendExtDirectRequest([
-                                               Workspaces.generateExtDirectActionsPayload(stageExecuteAction, [serializedForm]),
-                                               Workspaces.generateExtDirectPayload('getWorkspaceInfos', Backend.settings)
+                                       Workspaces.sendRemoteRequest([
+                                               Workspaces.generateRemoteActionsPayload(stageExecuteAction, [serializedForm]),
+                                               Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings)
                                        ]).done(function(response) {
                                                $modal.modal('hide');
                                                Backend.renderWorkspaceInfos(response[1].result);
@@ -413,11 +413,11 @@ define([
         * Loads the workspace components, like available stage actions and items of the workspace
         */
        Backend.loadWorkspaceComponents = function() {
-               Workspaces.sendExtDirectRequest([
-                       Workspaces.generateExtDirectPayload('getWorkspaceInfos', Backend.settings),
-                       Workspaces.generateExtDirectPayload('getStageActions', {}),
-                       Workspaces.generateExtDirectMassActionsPayload('getMassStageActions', {}),
-                       Workspaces.generateExtDirectPayload('getSystemLanguages', {})
+               Workspaces.sendRemoteRequest([
+                       Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings),
+                       Workspaces.generateRemotePayload('getStageActions', {}),
+                       Workspaces.generateRemoteMassActionsPayload('getMassStageActions', {}),
+                       Workspaces.generateRemotePayload('getSystemLanguages', {})
                ]).done(function(response) {
                        Backend.elements.$depthSelector.prop('disabled', false);
 
@@ -466,8 +466,8 @@ define([
         * @protected
         */
        Backend.getWorkspaceInfos = function() {
-               Workspaces.sendExtDirectRequest(
-                       Workspaces.generateExtDirectPayload('getWorkspaceInfos', Backend.settings)
+               Workspaces.sendRemoteRequest(
+                       Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings)
                ).done(function(response) {
                        Backend.renderWorkspaceInfos(response[0].result);
                });
@@ -627,8 +627,8 @@ define([
 
                var $tr = $(e.target).closest('tr');
 
-               Workspaces.sendExtDirectRequest(
-                       Workspaces.generateExtDirectPayload('getRowDetails', {
+               Workspaces.sendRemoteRequest(
+                       Workspaces.generateRemotePayload('getRowDetails', {
                                stage: $tr.data('stage'),
                                t3ver_oid: $tr.data('t3ver_oid'),
                                table: $tr.data('table'),
@@ -756,8 +756,8 @@ define([
        Backend.openPreview = function(e) {
                var $tr = $(e.target).closest('tr');
 
-               Workspaces.sendExtDirectRequest(
-                       Workspaces.generateExtDirectActionsPayload('viewSingleRecord', [
+               Workspaces.sendRemoteRequest(
+                       Workspaces.generateRemoteActionsPayload('viewSingleRecord', [
                                $tr.data('table'), $tr.data('uid')
                        ])
                ).done(function(response) {
@@ -849,8 +849,8 @@ define([
                );
                $modal.on('button.clicked', function(e) {
                        if (e.target.name === 'ok') {
-                               Workspaces.sendExtDirectRequest([
-                                       Workspaces.generateExtDirectActionsPayload('deleteSingleRecord', [
+                               Workspaces.sendRemoteRequest([
+                                       Workspaces.generateRemoteActionsPayload('deleteSingleRecord', [
                                                $tr.data('table'),
                                                $tr.data('uid')
                                        ])
@@ -930,8 +930,8 @@ define([
                        Severity.warning
                );
                Wizard.addFinalProcessingSlide(function() {
-                       Workspaces.sendExtDirectRequest(
-                               Workspaces.generateExtDirectActionsPayload('executeSelectionAction', {
+                       Workspaces.sendRemoteRequest(
+                               Workspaces.generateRemoteActionsPayload('executeSelectionAction', {
                                        action: selectedAction,
                                        selection: affectedRecords
                                })
@@ -1014,8 +1014,8 @@ define([
                        Severity.warning
                );
                Wizard.addFinalProcessingSlide(function() {
-                       Workspaces.sendExtDirectRequest(
-                               Workspaces.generateExtDirectMassActionsPayload(massAction, {
+                       Workspaces.sendRemoteRequest(
+                               Workspaces.generateRemoteMassActionsPayload(massAction, {
                                        init: true,
                                        total: 0,
                                        processed: 0,
@@ -1024,8 +1024,8 @@ define([
                                })
                        ).done(function(response) {
                                var payload = response[0].result;
-                               Workspaces.sendExtDirectRequest(
-                                       Workspaces.generateExtDirectMassActionsPayload(massAction, payload)
+                               Workspaces.sendRemoteRequest(
+                                       Workspaces.generateRemoteMassActionsPayload(massAction, payload)
                                ).done(function() {
                                        Backend.getWorkspaceInfos();
                                        Wizard.dismiss();
@@ -1056,8 +1056,8 @@ define([
                                t3ver_oid: affected[2]
                        });
                }
-               Workspaces.sendExtDirectRequest(
-                       Workspaces.generateExtDirectActionsPayload('sendToSpecificStageWindow', [
+               Workspaces.sendRemoteRequest(
+                       Workspaces.generateRemoteActionsPayload('sendToSpecificStageWindow', [
                                stage, affectedRecords
                        ])
                ).done(function(response) {
@@ -1072,9 +1072,9 @@ define([
                                                nextStage: stage
                                        };
 
-                                       Workspaces.sendExtDirectRequest([
-                                               Workspaces.generateExtDirectActionsPayload('sendToSpecificStageExecute', [serializedForm]),
-                                               Workspaces.generateExtDirectPayload('getWorkspaceInfos', Backend.settings)
+                                       Workspaces.sendRemoteRequest([
+                                               Workspaces.generateRemoteActionsPayload('sendToSpecificStageExecute', [serializedForm]),
+                                               Workspaces.generateRemotePayload('getWorkspaceInfos', Backend.settings)
                                        ]).done(function(response) {
                                                $modal.modal('hide');
                                                Backend.renderWorkspaceInfos(response[1].result);
@@ -1098,7 +1098,6 @@ define([
 
        /**
         * Renders the action button based on the user's permission.
-        * This method is intended to be dropped once we don't the ExtDirect stuff anymore.
         *
         * @returns {$}
         * @private
@@ -1114,8 +1113,8 @@ define([
         * Fetches and renders available preview links
         */
        Backend.generatePreviewLinks = function() {
-               Workspaces.sendExtDirectRequest(
-                       Workspaces.generateExtDirectActionsPayload('generateWorkspacePreviewLinksForAllLanguages', [
+               Workspaces.sendRemoteRequest(
+                       Workspaces.generateRemoteActionsPayload('generateWorkspacePreviewLinksForAllLanguages', [
                                Backend.settings.id
                        ])
                ).done(function(response) {
@@ -1189,4 +1188,4 @@ define([
        };
 
        $(Backend.initialize);
-});
\ No newline at end of file
+});
index 1c11134..78c2188 100644 (file)
@@ -170,9 +170,9 @@ define([
                );
                $modal.on('button.clicked', function(e) {
                        if (e.target.name === 'ok') {
-                               Workspaces.sendExtDirectRequest([
-                                       Workspaces.generateExtDirectActionsPayload('discardStagesFromPage', [TYPO3.settings.Workspaces.id]),
-                                       Workspaces.generateExtDirectActionsPayload('updateStageChangeButtons', [TYPO3.settings.Workspaces.id])
+                               Workspaces.sendRemoteRequest([
+                                       Workspaces.generateRemoteActionsPayload('discardStagesFromPage', [TYPO3.settings.Workspaces.id]),
+                                       Workspaces.generateRemoteActionsPayload('updateStageChangeButtons', [TYPO3.settings.Workspaces.id])
                                ]).done(function(response) {
                                        $modal.modal('hide');
                                        Preview.renderStageButtons(response[1].result);
@@ -220,8 +220,8 @@ define([
                        throw 'Invalid direction ' + direction + ' requested.';
                }
 
-               Workspaces.sendExtDirectRequest(
-                       Workspaces.generateExtDirectActionsPayload(actionName, [TYPO3.settings.Workspaces.id])
+               Workspaces.sendRemoteRequest(
+                       Workspaces.generateRemoteActionsPayload(actionName, [TYPO3.settings.Workspaces.id])
                ).done(function(response) {
                        var $modal = Workspaces.renderSendToStageWindow(response);
                        $modal.on('button.clicked', function (e) {
@@ -232,9 +232,9 @@ define([
                                        serializedForm.affects = response[0].result.affects;
                                        serializedForm.stageId = $me.data('stageId');
 
-                                       Workspaces.sendExtDirectRequest([
-                                               Workspaces.generateExtDirectActionsPayload('sentCollectionToStage', [serializedForm]),
-                                               Workspaces.generateExtDirectActionsPayload('updateStageChangeButtons', [TYPO3.settings.Workspaces.id])
+                                       Workspaces.sendRemoteRequest([
+                                               Workspaces.generateRemoteActionsPayload('sentCollectionToStage', [serializedForm]),
+                                               Workspaces.generateRemoteActionsPayload('updateStageChangeButtons', [TYPO3.settings.Workspaces.id])
                                        ]).done(function(response) {
                                                $modal.modal('hide');
 
@@ -302,4 +302,4 @@ define([
        $(document).ready(function() {
                Preview.initialize();
        });
-});
\ No newline at end of file
+});
index bc4e17d..2a57f0d 100644 (file)
@@ -108,7 +108,7 @@ define(['jquery'], function($) {
         * @param {String} title the workspace title
         */
        WorkspacesMenu.performWorkspaceSwitch = function(id, title) {
-               top.TYPO3.Workspaces.workspaceTitle = title;
+               top.TYPO3.Backend.workspaceTitle = title;
                top.TYPO3.configuration.inWorkspace = id !== 0;
 
                WorkspacesMenu.updateBackendContext(title);
index 60e20ef..52f50e9 100644 (file)
@@ -22,7 +22,9 @@ define([
 ], function($, Severity, Modal) {
        'use strict';
 
-       var Workspaces = {};
+       var Workspaces = {
+               tid: 0
+       };
 
        /**
         * Renders the send to stage window
@@ -99,21 +101,20 @@ define([
         * @return {$}
         */
        Workspaces.checkIntegrity = function(payload) {
-               return Workspaces.sendExtDirectRequest(
-                       Workspaces.generateExtDirectPayload('checkIntegrity', payload)
+               return Workspaces.sendRemoteRequest(
+                       Workspaces.generateRemotePayload('checkIntegrity', payload)
                );
        };
 
        /**
-        * Sends an AJAX request compatible to ExtDirect
-        * This method is intended to be dropped once we don't the ExtDirect stuff anymore.
+        * Sends an AJAX request
         *
         * @param {Object} payload
         * @return {$}
         */
-       Workspaces.sendExtDirectRequest = function(payload) {
+       Workspaces.sendRemoteRequest = function(payload) {
                return $.ajax({
-                       url: TYPO3.settings.ajaxUrls['ext_direct_route'] + '&namespace=TYPO3.Workspaces',
+                       url: TYPO3.settings.ajaxUrls['workspace_dispatch'],
                        method: 'POST',
                        contentType: 'application/json; charset=utf-8',
                        dataType: 'json',
@@ -122,45 +123,45 @@ define([
        };
 
        /**
-        * Generates the payload for ExtDirect
+        * Generates the payload for a remote call
         *
         * @param {String} method
         * @param {Object} data
         * @return {{action, data, method, type}}
         */
-       Workspaces.generateExtDirectPayload = function(method, data) {
+       Workspaces.generateRemotePayload = function(method, data) {
                if (typeof data === 'undefined') {
                        data = {};
                }
-               return Workspaces.generateExtDirectPayloadBody('ExtDirect', method, data);
+               return Workspaces.generateRemotePayloadBody('RemoteServer', method, data);
        };
 
        /**
-        * Generates the payload for ExtDirectMassActions
+        * Generates the payload for MassActions
         *
         * @param {String} method
         * @param {Object} data
         * @return {{action, data, method, type}}
         */
-       Workspaces.generateExtDirectMassActionsPayload = function(method, data) {
+       Workspaces.generateRemoteMassActionsPayload = function(method, data) {
                if (typeof data === 'undefined') {
                        data = {};
                }
-               return Workspaces.generateExtDirectPayloadBody('ExtDirectMassActions', method, data);
+               return Workspaces.generateRemotePayloadBody('MassActions', method, data);
        };
 
        /**
-        * Generates the payload for ExtDirectActions
+        * Generates the payload for Actions
         *
         * @param {String} method
         * @param {Object} data
         * @return {{action, data, method, type}}
         */
-       Workspaces.generateExtDirectActionsPayload = function(method, data) {
+       Workspaces.generateRemoteActionsPayload = function(method, data) {
                if (typeof data === 'undefined') {
                        data = [];
                }
-               return Workspaces.generateExtDirectPayloadBody('ExtDirectActions', method, data);
+               return Workspaces.generateRemotePayloadBody('Actions', method, data);
        };
 
        /**
@@ -171,7 +172,7 @@ define([
         * @param {Object} data
         * @return {{action: String, data: Object, method: String, type: string}}
         */
-       Workspaces.generateExtDirectPayloadBody = function(action, method, data) {
+       Workspaces.generateRemotePayloadBody = function(action, method, data) {
                if (data instanceof Array) {
                        data.push(TYPO3.settings.Workspaces.token);
                } else {
@@ -184,7 +185,8 @@ define([
                        action: action,
                        data: data,
                        method: method,
-                       type: 'rpc'
+                       type: 'rpc',
+                       tid: Workspaces.tid++
                };
        };
 
@@ -211,4 +213,4 @@ define([
        };
 
        return Workspaces;
-});
\ No newline at end of file
+});
index 7275d14..07e3cae 100644 (file)
@@ -40,7 +40,7 @@ class ActionHandlerTest extends \TYPO3\CMS\Core\Tests\FunctionalTestCase
      */
     public function sendToSpecificStageExecuteIgnoresDoublePublishes()
     {
-        $actionHandler = new \TYPO3\CMS\Workspaces\ExtDirect\ActionHandler();
+        $actionHandler = new \TYPO3\CMS\Workspaces\Controller\Remote\ActionHandler();
 
         $this->importDataSet(__DIR__ . '/Fixtures/pages.xml');
         $this->importDataSet(__DIR__ . '/Fixtures/sys_workspace.xml');
diff --git a/typo3/sysext/workspaces/Tests/Unit/Controller/Remote/RemoteServerTest.php b/typo3/sysext/workspaces/Tests/Unit/Controller/Remote/RemoteServerTest.php
new file mode 100644 (file)
index 0000000..2edb0f0
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+namespace TYPO3\CMS\Workspaces\Tests\Unit\Controller\Remote;
+
+/*
+ * 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 Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
+use TYPO3\CMS\Core\Resource\File;
+use TYPO3\CMS\Core\Resource\FileReference;
+use TYPO3\CMS\Core\Resource\ProcessedFile;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * RemoteServer test
+ */
+class RemoteServerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
+{
+    /**
+     * @var \TYPO3\CMS\Workspaces\Controller\Remote\RemoteServer
+     */
+    protected $subject;
+
+    /**
+     * @var FileReference[]|ObjectProphecy[]
+     */
+    protected $fileReferenceProphecies;
+
+    /**
+     * Set up
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+        $this->subject = $this->getAccessibleMock(\TYPO3\CMS\Workspaces\Controller\Remote\RemoteServer::class, ['__none']);
+    }
+
+    /**
+     * Tear down.
+     */
+    protected function tearDown()
+    {
+        parent::tearDown();
+        unset($this->subject);
+        unset($this->fileReferenceProphecies);
+    }
+
+    /**
+     * @return array
+     */
+    public function prepareFileReferenceDifferencesAreCorrectDataProvider()
+    {
+        return [
+            // without thumbnails
+            'unchanged wo/thumbnails' => ['1,2,3,4', '1,2,3,4', false, null],
+            'front addition wo/thumbnails' => ['1,2,3,4', '99,1,2,3,4', false, [
+                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
+                'differences' => '<ins>/img/99.png </ins>/img/1.png /img/2.png /img/3.png /img/4.png',
+            ]],
+            'end addition wo/thumbnails' => ['1,2,3,4', '1,2,3,4,99', false, [
+                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
+                'differences' => '/img/1.png /img/2.png /img/3.png /img/4.png <ins>/img/99.png </ins>',
+            ]],
+            'reorder wo/thumbnails' => ['1,2,3,4', '1,3,2,4', false, [
+                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
+                'differences' => '/img/1.png <ins>/img/3.png </ins>/img/2.png <del>/img/3.png </del>/img/4.png',
+            ]],
+            'move to end wo/thumbnails' => ['1,2,3,4', '2,3,4,1', false, [
+                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
+                'differences' => '<del>/img/1.png </del>/img/2.png /img/3.png /img/4.png <ins>/img/1.png </ins>',
+            ]],
+            'move to front wo/thumbnails' => ['1,2,3,4', '4,1,2,3', false, [
+                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
+                'differences' => '<ins>/img/4.png </ins>/img/1.png /img/2.png /img/3.png <del>/img/4.png </del>',
+            ]],
+            'keep last wo/thumbnails' => ['1,2,3,4', '4', false, [
+                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
+                'differences' => '<del>/img/1.png /img/2.png /img/3.png </del>/img/4.png',
+            ]],
+            // with thumbnails
+            'unchanged w/thumbnails' => ['1,2,3,4', '1,2,3,4', true, null],
+            'front addition w/thumbnails' => ['1,2,3,4', '99,1,2,3,4', true, [
+                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
+                'differences' => '<ins><img src="/tmb/99.png" /> </ins><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
+            ]],
+            'end addition w/thumbnails' => ['1,2,3,4', '1,2,3,4,99', true, [
+                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
+                'differences' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" /> <ins><img src="/tmb/99.png" /> </ins>',
+            ]],
+            'reorder w/thumbnails' => ['1,2,3,4', '1,3,2,4', true, [
+                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
+                'differences' => '<img src="/tmb/1.png" /> <ins><img src="/tmb/3.png" /> </ins><img src="/tmb/2.png" /> <del><img src="/tmb/3.png" /> </del><img src="/tmb/4.png" />',
+            ]],
+            'move to end w/thumbnails' => ['1,2,3,4', '2,3,4,1', true, [
+                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
+                'differences' => '<del><img src="/tmb/1.png" /> </del><img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" /> <ins><img src="/tmb/1.png" /> </ins>',
+            ]],
+            'move to front w/thumbnails' => ['1,2,3,4', '4,1,2,3', true, [
+                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
+                'differences' => '<ins><img src="/tmb/4.png" /> </ins><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <del><img src="/tmb/4.png" /> </del>',
+            ]],
+            'keep last w/thumbnails' => ['1,2,3,4', '4', true, [
+                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
+                'differences' => '<del><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> </del><img src="/tmb/4.png" />',
+            ]],
+        ];
+    }
+
+    /**
+     * @param string $fileFileReferenceList
+     * @param string $versionFileReferenceList
+     * @param $useThumbnails
+     * @param array|null $expected
+     * @dataProvider prepareFileReferenceDifferencesAreCorrectDataProvider
+     * @test
+     */
+    public function prepareFileReferenceDifferencesAreCorrect($fileFileReferenceList, $versionFileReferenceList, $useThumbnails, array $expected = null)
+    {
+        $liveFileReferences = $this->getFileReferenceProphecies($fileFileReferenceList);
+        $versionFileReferences = $this->getFileReferenceProphecies($versionFileReferenceList);
+
+        $result = $this->subject->_call(
+            'prepareFileReferenceDifferences',
+            $liveFileReferences,
+            $versionFileReferences,
+            $useThumbnails
+        );
+
+        $this->assertSame($expected, $result);
+    }
+
+    /**
+     * @param string $idList List of ids
+     * @return FileReference[]|ObjectProphecy[]
+     */
+    protected function getFileReferenceProphecies($idList)
+    {
+        $fileReferenceProphecies = [];
+        $ids = GeneralUtility::trimExplode(',', $idList, true);
+
+        foreach ($ids as $id) {
+            $fileReferenceProphecies[$id] = $this->getFileReferenceProphecy($id);
+        }
+
+        return $fileReferenceProphecies;
+    }
+
+    /**
+     * @param int $id
+     * @return ObjectProphecy|FileReference
+     */
+    protected function getFileReferenceProphecy($id)
+    {
+        if (isset($this->fileReferenceProphecies[$id])) {
+            return $this->fileReferenceProphecies[$id];
+        }
+
+        $processedFileProphecy = $this->prophesize(ProcessedFile::class);
+        $processedFileProphecy->getPublicUrl(Argument::cetera())->willReturn('/tmb/' . $id . '.png');
+
+        $fileProphecy = $this->prophesize(File::class);
+        $fileProphecy->process(Argument::cetera())->willReturn($processedFileProphecy->reveal());
+
+        $fileReferenceProphecy = $this->prophesize(FileReference::class);
+        $fileReferenceProphecy->getUid()->willReturn($id);
+        $fileReferenceProphecy->getOriginalFile()->willReturn($fileProphecy->reveal());
+        $fileReferenceProphecy->getPublicUrl(Argument::cetera())->willReturn('/img/' . $id . '.png');
+
+        $this->fileReferenceProphecies[$id] = $fileReferenceProphecy->reveal();
+        return $this->fileReferenceProphecies[$id];
+    }
+}
diff --git a/typo3/sysext/workspaces/Tests/Unit/ExtDirect/ExtDirectServerTest.php b/typo3/sysext/workspaces/Tests/Unit/ExtDirect/ExtDirectServerTest.php
deleted file mode 100644 (file)
index 354feee..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-<?php
-namespace TYPO3\CMS\Workspaces\Tests\Unit\ExtDirect;
-
-/*
- * 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 Prophecy\Argument;
-use Prophecy\Prophecy\ObjectProphecy;
-use TYPO3\CMS\Core\Resource\File;
-use TYPO3\CMS\Core\Resource\FileReference;
-use TYPO3\CMS\Core\Resource\ProcessedFile;
-use TYPO3\CMS\Core\Utility\GeneralUtility;
-
-/**
- * ExtDirectServer test
- */
-class ExtDirectServerTest extends \TYPO3\CMS\Core\Tests\UnitTestCase
-{
-    /**
-     * @var \TYPO3\CMS\Workspaces\ExtDirect\ExtDirectServer
-     */
-    protected $subject;
-
-    /**
-     * @var FileReference[]|ObjectProphecy[]
-     */
-    protected $fileReferenceProphecies;
-
-    /**
-     * Set up
-     */
-    protected function setUp()
-    {
-        parent::setUp();
-        $this->subject = $this->getAccessibleMock(\TYPO3\CMS\Workspaces\ExtDirect\ExtDirectServer::class, ['__none']);
-    }
-
-    /**
-     * Tear down.
-     */
-    protected function tearDown()
-    {
-        parent::tearDown();
-        unset($this->subject);
-        unset($this->fileReferenceProphecies);
-    }
-
-    /**
-     * @return array
-     */
-    public function prepareFileReferenceDifferencesAreCorrectDataProvider()
-    {
-        return [
-            // without thumbnails
-            'unchanged wo/thumbnails' => ['1,2,3,4', '1,2,3,4', false, null],
-            'front addition wo/thumbnails' => ['1,2,3,4', '99,1,2,3,4', false, [
-                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
-                'differences' => '<ins>/img/99.png </ins>/img/1.png /img/2.png /img/3.png /img/4.png',
-            ]],
-            'end addition wo/thumbnails' => ['1,2,3,4', '1,2,3,4,99', false, [
-                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
-                'differences' => '/img/1.png /img/2.png /img/3.png /img/4.png <ins>/img/99.png </ins>',
-            ]],
-            'reorder wo/thumbnails' => ['1,2,3,4', '1,3,2,4', false, [
-                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
-                'differences' => '/img/1.png <ins>/img/3.png </ins>/img/2.png <del>/img/3.png </del>/img/4.png',
-            ]],
-            'move to end wo/thumbnails' => ['1,2,3,4', '2,3,4,1', false, [
-                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
-                'differences' => '<del>/img/1.png </del>/img/2.png /img/3.png /img/4.png <ins>/img/1.png </ins>',
-            ]],
-            'move to front wo/thumbnails' => ['1,2,3,4', '4,1,2,3', false, [
-                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
-                'differences' => '<ins>/img/4.png </ins>/img/1.png /img/2.png /img/3.png <del>/img/4.png </del>',
-            ]],
-            'keep last wo/thumbnails' => ['1,2,3,4', '4', false, [
-                'live' => '/img/1.png /img/2.png /img/3.png /img/4.png',
-                'differences' => '<del>/img/1.png /img/2.png /img/3.png </del>/img/4.png',
-            ]],
-            // with thumbnails
-            'unchanged w/thumbnails' => ['1,2,3,4', '1,2,3,4', true, null],
-            'front addition w/thumbnails' => ['1,2,3,4', '99,1,2,3,4', true, [
-                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
-                'differences' => '<ins><img src="/tmb/99.png" /> </ins><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
-            ]],
-            'end addition w/thumbnails' => ['1,2,3,4', '1,2,3,4,99', true, [
-                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
-                'differences' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" /> <ins><img src="/tmb/99.png" /> </ins>',
-            ]],
-            'reorder w/thumbnails' => ['1,2,3,4', '1,3,2,4', true, [
-                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
-                'differences' => '<img src="/tmb/1.png" /> <ins><img src="/tmb/3.png" /> </ins><img src="/tmb/2.png" /> <del><img src="/tmb/3.png" /> </del><img src="/tmb/4.png" />',
-            ]],
-            'move to end w/thumbnails' => ['1,2,3,4', '2,3,4,1', true, [
-                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
-                'differences' => '<del><img src="/tmb/1.png" /> </del><img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" /> <ins><img src="/tmb/1.png" /> </ins>',
-            ]],
-            'move to front w/thumbnails' => ['1,2,3,4', '4,1,2,3', true, [
-                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
-                'differences' => '<ins><img src="/tmb/4.png" /> </ins><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <del><img src="/tmb/4.png" /> </del>',
-            ]],
-            'keep last w/thumbnails' => ['1,2,3,4', '4', true, [
-                'live' => '<img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> <img src="/tmb/4.png" />',
-                'differences' => '<del><img src="/tmb/1.png" /> <img src="/tmb/2.png" /> <img src="/tmb/3.png" /> </del><img src="/tmb/4.png" />',
-            ]],
-        ];
-    }
-
-    /**
-     * @param string $fileFileReferenceList
-     * @param string $versionFileReferenceList
-     * @param $useThumbnails
-     * @param array|null $expected
-     * @dataProvider prepareFileReferenceDifferencesAreCorrectDataProvider
-     * @test
-     */
-    public function prepareFileReferenceDifferencesAreCorrect($fileFileReferenceList, $versionFileReferenceList, $useThumbnails, array $expected = null)
-    {
-        $liveFileReferences = $this->getFileReferenceProphecies($fileFileReferenceList);
-        $versionFileReferences = $this->getFileReferenceProphecies($versionFileReferenceList);
-
-        $result = $this->subject->_call(
-            'prepareFileReferenceDifferences',
-            $liveFileReferences,
-            $versionFileReferences,
-            $useThumbnails
-        );
-
-        $this->assertSame($expected, $result);
-    }
-
-    /**
-     * @param string $idList List of ids
-     * @return FileReference[]|ObjectProphecy[]
-     */
-    protected function getFileReferenceProphecies($idList)
-    {
-        $fileReferenceProphecies = [];
-        $ids = GeneralUtility::trimExplode(',', $idList, true);
-
-        foreach ($ids as $id) {
-            $fileReferenceProphecies[$id] = $this->getFileReferenceProphecy($id);
-        }
-
-        return $fileReferenceProphecies;
-    }
-
-    /**
-     * @param int $id
-     * @return ObjectProphecy|FileReference
-     */
-    protected function getFileReferenceProphecy($id)
-    {
-        if (isset($this->fileReferenceProphecies[$id])) {
-            return $this->fileReferenceProphecies[$id];
-        }
-
-        $processedFileProphecy = $this->prophesize(ProcessedFile::class);
-        $processedFileProphecy->getPublicUrl(Argument::cetera())->willReturn('/tmb/' . $id . '.png');
-
-        $fileProphecy = $this->prophesize(File::class);
-        $fileProphecy->process(Argument::cetera())->willReturn($processedFileProphecy->reveal());
-
-        $fileReferenceProphecy = $this->prophesize(FileReference::class);
-        $fileReferenceProphecy->getUid()->willReturn($id);
-        $fileReferenceProphecy->getOriginalFile()->willReturn($fileProphecy->reveal());
-        $fileReferenceProphecy->getPublicUrl(Argument::cetera())->willReturn('/img/' . $id . '.png');
-
-        $this->fileReferenceProphecies[$id] = $fileReferenceProphecy->reveal();
-        return $this->fileReferenceProphecies[$id];
-    }
-}
index c9bc815..ad67131 100644 (file)
@@ -36,7 +36,7 @@ if (!is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations
 if (TYPO3_MODE === 'BE') {
     // If publishing/swapping dependent parent-child references, consider all parents and children
     \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addUserTSConfig('options.workspaces.considerReferences = 1');
-    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/tree/pagetree/class.t3lib_tree_pagetree_dataprovider.php']['postProcessCollections'][] = \TYPO3\CMS\Workspaces\ExtDirect\PagetreeCollectionsProcessor::class;
+    $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/tree/pagetree/class.t3lib_tree_pagetree_dataprovider.php']['postProcessCollections'][] = \TYPO3\CMS\Workspaces\Hooks\PagetreeCollectionsProcessor::class;
 
     $GLOBALS['TYPO3_CONF_VARS']['BE']['toolbarItems'][1435433114] = \TYPO3\CMS\Workspaces\Backend\ToolbarItems\WorkspaceSelectorToolbarItem::class;
 }
index 7f20536..d7da9ac 100644 (file)
@@ -21,26 +21,6 @@ if (TYPO3_MODE === 'BE' && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_INSTALL)) {
             'navigationComponentId' => 'typo3-pagetree'
         ]
     );
-
-    // register ExtDirect
-    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerExtDirectComponent(
-        'TYPO3.Workspaces.ExtDirect',
-        \TYPO3\CMS\Workspaces\ExtDirect\ExtDirectServer::class,
-        'web_WorkspacesWorkspaces',
-        'user,group'
-    );
-    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerExtDirectComponent(
-        'TYPO3.Workspaces.ExtDirectActions',
-        \TYPO3\CMS\Workspaces\ExtDirect\ActionHandler::class,
-        'web_WorkspacesWorkspaces',
-        'user,group'
-    );
-    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerExtDirectComponent(
-        'TYPO3.Workspaces.ExtDirectMassActions',
-        \TYPO3\CMS\Workspaces\ExtDirect\MassActionHandler::class,
-        'web_WorkspacesWorkspaces',
-        'user,group'
-    );
 }
 
 // Registers preview link icon